Skip to content

Commit a7f84c2

Browse files
committed
Merge branch '5.0/multipart-related-display' into 5.0-trunk
2 parents 6b299b5 + 58d6c2f commit a7f84c2

4 files changed

Lines changed: 160 additions & 2 deletions

File tree

share/html/Elements/ShowTransactionAttachments

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,8 +220,20 @@ my $render_attachment = sub {
220220
# it's a toplevel object
221221
!$ParentObj
222222

223-
# or its parent isn't a multipart alternative
224-
|| ( $ParentObj->ContentType !~ m{^multipart/(?:alternative|related)$}i )
223+
# or it isn't part of a multipart/alternative grouping. A
224+
# multipart/related is only an alternative representation when it
225+
# is itself nested inside a multipart/alternative (e.g. the rich
226+
# side of alternative -> [text/plain, related -> text/html]); a
227+
# standalone multipart/related is not an alternative container per
228+
# RFC 2387, so its root textual part must always be displayed.
229+
|| (
230+
$ParentObj->ContentType !~ m{^multipart/alternative$}i
231+
&& !(
232+
$ParentObj->ContentType =~ m{^multipart/related$}i
233+
&& $ParentObj->ParentObj
234+
&& $ParentObj->ParentObj->ContentType =~ m{^multipart/alternative$}i
235+
)
236+
)
225237

226238
# or it's of our prefered alterative type
227239
|| (
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
Subject: multipart/alternative wrapping a multipart/related html part
2+
From: root@localhost
3+
To: rt@example.com
4+
Content-Type: multipart/alternative; boundary="=_alt_boundary"
5+
6+
This is a multipart message in MIME format.
7+
8+
--=_alt_boundary
9+
Content-Type: text/plain; charset="UTF-8"
10+
Content-Transfer-Encoding: 7bit
11+
12+
PLAINDOWNGRADEMARKER this is the plain-text downgrade of the rich message.
13+
14+
--=_alt_boundary
15+
Content-Type: multipart/related; boundary="=_rel_boundary"
16+
17+
--=_rel_boundary
18+
Content-Type: text/html; charset="UTF-8"
19+
Content-Transfer-Encoding: 7bit
20+
21+
<div>HTMLBODYMARKER this is the composed rich body.<br />
22+
<img src="cid:inline-image@example.com" /></div>
23+
24+
--=_rel_boundary
25+
Content-Type: image/png; name="inline.png"
26+
Content-Transfer-Encoding: base64
27+
Content-Disposition: inline; filename="inline.png"
28+
Content-ID: <inline-image@example.com>
29+
30+
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M8A
31+
AAMBAQDJ/pLvAAAAAElFTkSuQmCC
32+
33+
--=_rel_boundary--
34+
35+
--=_alt_boundary--
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
Subject: multipart/related with a text/plain root
2+
From: root@localhost
3+
To: rt@example.com
4+
Content-Type: multipart/related; boundary="=_related_boundary_42"
5+
6+
This is a multipart message in MIME format.
7+
8+
--=_related_boundary_42
9+
Content-Type: text/plain; charset="UTF-8"
10+
Content-Transfer-Encoding: 7bit
11+
12+
UNIQUEPLAINBODY this is the plain text body of a multipart/related message that
13+
has no text/html alternative part. It must still be displayed.
14+
15+
--=_related_boundary_42
16+
Content-Type: application/octet-stream; name="data1.bin"
17+
Content-Transfer-Encoding: base64
18+
Content-Disposition: attachment; filename="data1.bin"
19+
20+
aGVsbG8gd29ybGQK
21+
22+
--=_related_boundary_42
23+
Content-Type: application/octet-stream; name="data2.bin"
24+
Content-Transfer-Encoding: base64
25+
Content-Disposition: attachment; filename="data2.bin"
26+
27+
Z29vZGJ5ZSB3b3JsZAo=
28+
29+
--=_related_boundary_42--

t/web/ticket_txn_content.t

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,4 +247,86 @@ This is the original forwarded email
247247
</div></blockquote></div><hr class="clear"></div></div>
248248
EOF
249249

250+
diag "multipart/related with a text/plain root (no text/html) must still display the body";
251+
{
252+
my $path
253+
= RT::Test::get_relocatable_file( 'multipart-related-text-plain', ( File::Spec->updir(), 'data', 'emails' ) );
254+
my $email = RT::Test->file_content($path);
255+
256+
my ( $status, $id ) = RT::Test->send_via_mailgate($email);
257+
is( $status >> 8, 0, 'mail gateway exited normally' );
258+
ok( $id, "created ticket #$id from multipart/related message" );
259+
260+
my $ticket = RT::Ticket->new( RT->SystemUser );
261+
$ticket->Load($id);
262+
is( $ticket->Id, $id, "loaded ticket #$id" );
263+
264+
# Confirm the message really has the structure under test before asserting
265+
# on how it is displayed.
266+
my $txn = $ticket->Transactions->First;
267+
my $top = $txn->Attachments->First;
268+
is( lc $top->ContentType, 'multipart/related', 'top-level part is multipart/related' );
269+
270+
my %child_types = map { lc( $_->ContentType ) => 1 }
271+
grep { $_->Parent == $top->Id } @{ $txn->Attachments->ItemsArrayRef };
272+
ok( $child_types{'text/plain'}, 'has a text/plain child part' );
273+
ok( !$child_types{'text/html'}, 'has no text/html child part' );
274+
275+
$m->get_ok( "/Ticket/History.html?id=$id", 'fetched ticket history' );
276+
277+
my @bodies = $m->dom->find('div.messagebody')->map('all_text')->each;
278+
ok( ( grep {/UNIQUEPLAINBODY/} @bodies ), 'text/plain body of the multipart/related message is displayed inline' )
279+
or diag "Rendered message bodies: " . join( '||', @bodies );
280+
}
281+
282+
# Returns the concatenated text of every rendered message body for a ticket.
283+
sub history_bodies {
284+
my $tid = shift;
285+
$m->get_ok( "/Ticket/History.html?id=$tid", "fetched history for #$tid" );
286+
return join '||', $m->dom->find('div.messagebody')->map('all_text')->each;
287+
}
288+
289+
# Used by both the PreferRichText on/off cases below.
290+
my $alt_id;
291+
292+
diag "nested multipart/alternative -> multipart/related(html): exactly one textual part renders";
293+
{
294+
# The common MUA structure (Outlook, Apple Mail, Gmail):
295+
# multipart/alternative
296+
# text/plain # plain downgrade
297+
# multipart/related
298+
# text/html # composed rich body
299+
# image/png # inline CID image
300+
# The text/html part's direct parent is the multipart/related. RT must
301+
# show exactly one textual part, never both (the "duplicate message"
302+
# regression fixed in 4a38585f75) and never neither. With PreferRichText
303+
# on (the default) the rich body wins.
304+
my $path = RT::Test::get_relocatable_file( 'multipart-alternative-related-html',
305+
( File::Spec->updir(), 'data', 'emails' ) );
306+
my $email = RT::Test->file_content($path);
307+
308+
my ( $status, $id ) = RT::Test->send_via_mailgate($email);
309+
is( $status >> 8, 0, 'mail gateway exited normally' );
310+
ok( $id, "created ticket #$id from nested alternative/related message" );
311+
$alt_id = $id;
312+
313+
my $bodies = history_bodies($id);
314+
like( $bodies, qr/HTMLBODYMARKER/, 'PreferRichText on: text/html body is displayed' );
315+
unlike( $bodies, qr/PLAINDOWNGRADEMARKER/, 'PreferRichText on: text/plain downgrade is suppressed (no duplicate)' );
316+
}
317+
318+
diag "same message with PreferRichText off: the plain downgrade wins";
319+
{
320+
# PreferRichText is overridable, so flip it via the user preference rather
321+
# than restarting the server with a new system config.
322+
my $root = RT::User->new( RT->SystemUser );
323+
$root->Load('root');
324+
my ( $ok, $msg ) = $root->SetPreferences( RT->System, { PreferRichText => 0 } );
325+
ok( $ok, "set root's PreferRichText preference to 0" ) or diag $msg;
326+
327+
my $bodies = history_bodies($alt_id);
328+
like( $bodies, qr/PLAINDOWNGRADEMARKER/, 'PreferRichText off: text/plain downgrade is displayed' );
329+
unlike( $bodies, qr/HTMLBODYMARKER/, 'PreferRichText off: text/html body is suppressed (no duplicate)' );
330+
}
331+
250332
done_testing;

0 commit comments

Comments
 (0)