MIME multipart boundaries and S/MIME

John Gardiner Myers <[email protected]> Thu, 23 Jun 2005 13:24:35 -0700
Newsgroups gmane.comp.lang.perl.modules.mail-box
Message-ID <[email protected]>
This is a multi-part message in MIME format.
--------------070903000501090303060605
Content-Type: text/plain; charset=ISO-8859-1; format=flowed
Content-Transfer-Encoding: 7bit

I'm trying to fix the problem where Mail::Box breaks the signature of 
some S/MIME messages.  This requires fairly extensive changes to fix, so 
I'd like to get your input on how to design the fix.

The problem stems from the failure of Mail::Box to conform to the 
following paragraph in section 5.1.1 of RFC 2046:

   NOTE:  The CRLF preceding the boundary delimiter line is conceptually
   attached to the boundary so that it is possible to have a part that
   does not end with a CRLF (line  break).  Body parts that must be
   considered to end with line breaks, therefore, must have two CRLFs
   preceding the boundary delimiter line, the first of which is part of
   the preceding body part, and the second of which is part of the
   encapsulation boundary.


The most common problem is that 
Mail::Box::Parser::Perl::_read_stripped_lines will remove a blank line 
before a boundary delimiter line, but 
Mail::Message::Body::Multipart::lines will not put it back in.  
Mail::Message:Body::Multipart::lines is clearly wrong--in order to 
conform to the MIME standard, it needs to insert a CRLF before any 
boundary delimiter line preceeded by a body-part.

Mail::Box::Parser::Perl::_read_stripped_lines is also incorrect in only 
removing the CRLF before the boundary delimiter line when that CRLF is a 
blank line.  As quoted above and as can be deduced from the MIME 
grammar, it is possible to have a part that does not end with a CRLF.  
In the case where the line before a boundary delimiter line is not 
blank, the @lines array for the preceeding body needs to end with a line 
with no trailing CRLF.  This loosens the previously held invariant that 
every line in a @lines array ends with CRLF, allowing the last line in a 
@lines array to omit a CRLF.  This brings up the first design question: 
should the nrLines() method  include an unterminated final line in its 
returned count?  If so, then Mail::Message::Body::Multipart::nrLines has 
to peek at the last line of every subpart to see if it is unterminated.  
If not, then leaf implementations of nrLines() need to peek at their 
respective last lines to see if they are unterminated.

Complicating this is the issue of preambles.  Per the multipart grammar, 
if there is no preamble, then it is not necessary to have a CRLF before 
the first boundary delimiter line.  I can see two ways of addressing 
this: consider any CRLF to be part of the preamble or somehow encode the 
difference between no preamble (no CRLF before first boundary delimiter 
line) and a 0-length preamble (one CRLF before first boundary delimiter 
line).  Either choice requires an interface change to 
Mail::Box::Parser::Perl::_read_stripped_lines and its immediate callers 
(bodyAsString, bodyAsList, bodyAsFile, and bodyDelayed).  The first 
choice requires an additional argument saying whether or not a preamble 
is being read, the second choice requires a way to return the difference 
between "stripped CRLF" and "no CRLF to strip".

Attached is my work so far.  I'm making the parser treat MIME multipart 
boundaries different than mailbox boundaries.  For mailbox boundaries, 
the lack of a blank line before the separator will not cause the CRLF to 
be stripped from the preceeding line.  I appreciate opinions on which 
direction to take this in.



--------------070903000501090303060605
Content-Type: text/plain;
 name="mailbox-signed2.diff"
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
 filename="mailbox-signed2.diff"

Only in Mail-Box-2.060-5signed2: blib
diff -ur Mail-Box-2.060-4version/lib/Mail/Box/Parser/Perl.pm Mail-Box-2.060-5signed2/lib/Mail/Box/Parser/Perl.pm
--- Mail-Box-2.060-4version/lib/Mail/Box/Parser/Perl.pm	2005-04-01 10:54:55.000000000 -0800
+++ Mail-Box-2.060-5signed2/lib/Mail/Box/Parser/Perl.pm	2005-06-10 14:47:05.000000000 -0700
@@ -102,14 +102,18 @@
     # No seps, then when have to trust it.
     my $sep = $self->{MBPP_separators}[0];
     return 1 unless defined $sep;
+    my $ismimemultipart = $sep =~ /^--/;
 
     my $file = $self->{MBPP_file};
     my $here = $file->tell;
-    $file->seek($where, 0) or return 0;
+    $file->seek($$where, 0) or return 0;
 
     # Find first non-empty line on specified location.
     my $line = $file->getline;
-    $line    = $file->getline while defined $line && $line =~ $empty;
+    while (defined $line && $line =~ $empty) {
+	$$where = $file->tell if $ismimemultipart;
+	$line    = $file->getline;
+    }
 
     # Check completed, return to old spot.
     $file->seek($here, 0);
@@ -197,19 +201,9 @@
     {   $lines = ((ref $file eq 'Mail::Box::FastScalar') || (ref $file eq 'Proofpoint::IO::FastScalar')) ? $file->getlines : [ $file->getlines ];
     }
 
+    pop @$lines if (@seps && $seps[0] !~ /^--/ && @$lines && ${$lines}[-1] =~ $empty);
+
     my $end = $file->tell;
-    if($exp_lines > 0 )
-    {    while(@$lines > $exp_lines && $lines->[-1] =~ $empty)
-         {   $end -= length $lines->[-1];
-             pop @$lines;
-         }
-    }
-    elsif(@seps && @$lines && $lines->[-1] =~ $empty)
-    {   # blank line should be in place before a separator.  Only that
-        # line is removed.
-        $end -= length $lines->[-1];
-        pop @$lines;
-    }
 
     map { s/^\>(\>*From\s)/$1/ } @$lines
         if $self->{MBPP_strip_gt};
@@ -241,7 +235,7 @@
     {   # Get at once may be successful
         my $end = $begin + $exp_chars;
 
-        if($self->_is_good_end($end))
+        if($self->_is_good_end(\$end))
         {   my $body = $self->_take_scalar($begin, $end);
             $body =~ s/^\>(\>*From\s)/$1/gm if $self->{MBPP_strip_gt};
             return ($begin, $file->tell, $body);
@@ -286,7 +280,7 @@
     if(defined $exp_chars)
     {   my $end = $begin + $exp_chars;
 
-        if($self->_is_good_end($end))
+        if($self->_is_good_end(\$end))
         {   $file->seek($end, 0);
             return ($begin, $end, $exp_chars, $exp_lines);
         }
Only in Mail-Box-2.060-5signed2/lib/Mail/Box/Parser: Perl.pm~
diff -ur Mail-Box-2.060-4version/lib/Mail/Message/Body/Multipart.pm Mail-Box-2.060-5signed2/lib/Mail/Message/Body/Multipart.pm
--- Mail-Box-2.060-4version/lib/Mail/Message/Body/Multipart.pm	2005-04-01 10:54:56.000000000 -0800
+++ Mail-Box-2.060-5signed2/lib/Mail/Message/Body/Multipart.pm	2005-06-13 11:25:43.000000000 -0700
@@ -134,8 +134,14 @@
     my $preamble = $self->preamble;
     push @lines, $preamble->lines if $preamble;
 
-    push @lines, "--$boundary\n", $_->lines
-        foreach $self->parts('ACTIVE');
+    foreach $self->parts('ACTIVE') {
+	push @lines, "--$boundary\n", $_->lines;
+	if ($lines[-1] =~ /\n$/) {
+	    push @lines, "\n";
+	} else {
+	    $lines[-1] .= "\n";
+	}
+    }
 
     push @lines, "--$boundary--\n";
 
@@ -169,6 +175,7 @@
     {   foreach my $part ($self->parts('ACTIVE'))
         {   print $out "--$boundary\n";
             $part->print($out);
+	    print $out "\n";
         }
         print $out "--$boundary--\n";
     }
@@ -176,6 +183,7 @@
     {   foreach my $part ($self->parts('ACTIVE'))
         {   $out->print("--$boundary\n");
             $part->print($out);
+	    $out->print("\n");
         }
         $out->print("--$boundary--\n");
     }
Only in Mail-Box-2.060-5signed2/lib/Mail/Message/Body: Multipart.pm~
Only in Mail-Box-2.060-5signed2: Makefile
Only in Mail-Box-2.060-5signed2: out.msg
Only in Mail-Box-2.060-5signed2: pm_to_blib
Only in Mail-Box-2.060-5signed2/tests/20pparser: 34bodymp.t~
Only in Mail-Box-2.060-5signed2/tests: a
Only in Mail-Box-2.060-5signed2/tests/folders: Mail
Only in Mail-Box-2.060-5signed2/tests/folders: maildir.src
Only in Mail-Box-2.060-5signed2/tests/folders: mbox.win
Only in Mail-Box-2.060-5signed2/tests/folders: mh.src
Only in Mail-Box-2.060-5signed2/tests: msg-7152-1.txt
Only in Mail-Box-2.060-5signed2/tests: msg-8617-1.txt

--------------070903000501090303060605--