I don't know why the access method is different. When doing "save-as", which fails, the imap response is streamed to a listener. When you do an open into an application, the imap response is fed to a "docshell" object. The decision point is here:
In both cases it appears that mime-part fetch on demand (MPOD) is forced back to true here:
because "part=..." appears in the URI. (Having MPOD configured as false is required to duplicate this bug.) With docshell (open to an app) the full message is re-fetched and the attachment is extracted OK (by libmime code, I think). But when a stream listener is in effect (save-as) only part 1 (the message body) is fetched so the saved information is corrupt because the attachment is never fetched at all.
The call to OpenCacheEntry() in nsImapProtocol.cpp "kicks-off" the imap access using the URI passed to it. The URI contains the proper "part=2" substring needed to request a fetch of the attachment. However, when nsImapUrl::GetImapPartToFetch(char** result) is called, it does not detect the "part=" string in the URI -- instead this function only looks for a "section=" substring and ignores the part= substring. Since there is no section= substring in the URI, the result is null so the attachment part is never fetched.
Looking at many URIs, I rarely, if ever, see "section=" when a multi-part message is accessed. But I always see "part=". So I don't know why only "section=" is looked for and "part=" is ignored in GetImapPartToFetch(). However, technically only the "section" is defined for email URI/URLs in https://tools.ietf.org/html/rfc5092; "part=" is not mentioned. In the tb libmime code, section is referred to as imappart while part is referred to as libmimepart. The imappart/section is required when fetching a message part using, e.g., . The libmimepart/part always seems to have "1." in front so section=2 corresponds to part=1.2.
The attached diff adds checking "part=" for when "section=" is not found. It leaves off the leading "X." for the returned part string. With this change the reporter's example email works properly with current default tb settings and part  is fetched, as it should, when saving and opening the attachment.
However, I'm not sure at all if this is a correct fix and why it was done like this to begin with. I can find no documentation on what libmimepart/part and imappart/section actually mean, how they are related or why they differ some. All I could find is a 20 year old reference basically wondering the same thing and possibly addressing a similar issue: bug 80347 comment 8. If anyone reading this can explain this I would appreciate it!
Edit P/s: There is also some discussion of GetImapPartToFetch() starting here bug 417480 comment 44. However "Part" is only used as a delimiter for the returned section. So it is looked at but never returned; only section (imappart) is returned and will be null unless the bodystructure for the message is fetched when the message is first opened. This will only occur when "fetch mime parts on demand" is enabled.