@@ -360,3 +360,235 @@ func TestIsEncryptedMessage_CaseInsensitive(t *testing.T) {
360360 })
361361 }
362362}
363+
364+ func TestExtractInlinePGP (t * testing.T ) {
365+ tests := []struct {
366+ name string
367+ rawMIME string
368+ wantContains string
369+ wantNil bool
370+ }{
371+ {
372+ name : "inline PGP in plain text body" ,
373+ rawMIME : `From: sender@example.com
374+ To: recipient@example.com
375+ Content-Type: text/plain
376+
377+ -----BEGIN PGP MESSAGE-----
378+
379+ hQEMAxxxxxxxxx
380+ =xxxx
381+ -----END PGP MESSAGE-----` ,
382+ wantContains : "-----BEGIN PGP MESSAGE-----" ,
383+ wantNil : false ,
384+ },
385+ {
386+ name : "inline PGP in multipart/mixed (Outlook style)" ,
387+ rawMIME : `From: sender@example.com
388+ Content-Type: multipart/mixed; boundary="boundary123"
389+
390+ --boundary123
391+ Content-Type: text/plain; charset="UTF-8"
392+
393+ -----BEGIN PGP MESSAGE-----
394+
395+ hQIMAwfdV3YDsnmWARAAs8jMMsaoLnlg
396+ =xxxx
397+ -----END PGP MESSAGE-----
398+ --boundary123--` ,
399+ wantContains : "-----BEGIN PGP MESSAGE-----" ,
400+ wantNil : false ,
401+ },
402+ {
403+ name : "inline PGP with surrounding text" ,
404+ rawMIME : `Content-Type: text/plain
405+
406+ Some text before
407+
408+ -----BEGIN PGP MESSAGE-----
409+ encrypted_data_here
410+ -----END PGP MESSAGE-----
411+
412+ Some text after` ,
413+ wantContains : "-----BEGIN PGP MESSAGE-----" ,
414+ wantNil : false ,
415+ },
416+ {
417+ name : "no PGP content" ,
418+ rawMIME : `From: sender@example.com
419+ Content-Type: text/plain
420+
421+ Just a regular email with no encryption.` ,
422+ wantNil : true ,
423+ },
424+ {
425+ name : "incomplete PGP block - missing end marker" ,
426+ rawMIME : `Content-Type: text/plain
427+
428+ -----BEGIN PGP MESSAGE-----
429+ encrypted_data_here
430+ No end marker` ,
431+ wantNil : true ,
432+ },
433+ {
434+ name : "PGP in second part of multipart" ,
435+ rawMIME : `Content-Type: multipart/mixed; boundary="mixed"
436+
437+ --mixed
438+ Content-Type: text/plain
439+
440+ Regular text part
441+ --mixed
442+ Content-Type: text/plain
443+
444+ -----BEGIN PGP MESSAGE-----
445+ encrypted_in_second_part
446+ -----END PGP MESSAGE-----
447+ --mixed--` ,
448+ wantContains : "-----BEGIN PGP MESSAGE-----" ,
449+ wantNil : false ,
450+ },
451+ {
452+ name : "PGP with CRLF line endings" ,
453+ rawMIME : "Content-Type: text/plain\r \n \r \n -----BEGIN PGP MESSAGE-----\r \n encrypted\r \n -----END PGP MESSAGE-----" ,
454+ wantContains : "-----BEGIN PGP MESSAGE-----" ,
455+ wantNil : false ,
456+ },
457+ {
458+ name : "empty input" ,
459+ rawMIME : "" ,
460+ wantNil : true ,
461+ },
462+ {
463+ name : "multipart without PGP" ,
464+ rawMIME : `Content-Type: multipart/mixed; boundary="b"
465+
466+ --b
467+ Content-Type: text/plain
468+
469+ No encryption here
470+ --b--` ,
471+ wantNil : true ,
472+ },
473+ }
474+
475+ for _ , tt := range tests {
476+ t .Run (tt .name , func (t * testing.T ) {
477+ got := extractInlinePGP (tt .rawMIME )
478+ if tt .wantNil {
479+ if got != nil {
480+ t .Errorf ("extractInlinePGP() = %q, want nil" , string (got ))
481+ }
482+ return
483+ }
484+ if got == nil {
485+ t .Error ("extractInlinePGP() = nil, want non-nil" )
486+ return
487+ }
488+ if ! strings .Contains (string (got ), tt .wantContains ) {
489+ t .Errorf ("extractInlinePGP() = %q, want to contain %q" , string (got ), tt .wantContains )
490+ }
491+ // Verify we extract the complete PGP block
492+ if ! strings .HasPrefix (string (got ), "-----BEGIN PGP MESSAGE-----" ) {
493+ t .Errorf ("extractInlinePGP() should start with PGP header, got: %q" , string (got ))
494+ }
495+ if ! strings .HasSuffix (string (got ), "-----END PGP MESSAGE-----" ) {
496+ t .Errorf ("extractInlinePGP() should end with PGP footer, got: %q" , string (got ))
497+ }
498+ })
499+ }
500+ }
501+
502+ func TestExtractInlinePGP_ExtractsCompletePGPBlock (t * testing.T ) {
503+ // Verify the extracted content is exactly the PGP block
504+ rawMIME := `Content-Type: text/plain
505+
506+ Preamble text here.
507+
508+ -----BEGIN PGP MESSAGE-----
509+
510+ hQEMAxxxxxxxxx
511+ line2
512+ line3
513+ =xxxx
514+ -----END PGP MESSAGE-----
515+
516+ Postamble text here.`
517+
518+ got := extractInlinePGP (rawMIME )
519+ if got == nil {
520+ t .Fatal ("extractInlinePGP() returned nil" )
521+ }
522+
523+ gotStr := string (got )
524+
525+ // Should not contain preamble or postamble
526+ if strings .Contains (gotStr , "Preamble" ) {
527+ t .Error ("extractInlinePGP() should not include preamble text" )
528+ }
529+ if strings .Contains (gotStr , "Postamble" ) {
530+ t .Error ("extractInlinePGP() should not include postamble text" )
531+ }
532+
533+ // Should contain the full PGP message
534+ if ! strings .Contains (gotStr , "hQEMAxxxxxxxxx" ) {
535+ t .Error ("extractInlinePGP() should contain the encrypted data" )
536+ }
537+ if ! strings .Contains (gotStr , "line2" ) {
538+ t .Error ("extractInlinePGP() should contain all lines of encrypted data" )
539+ }
540+ }
541+
542+ func TestExtractInlinePGP_Base64EncodedAttachment (t * testing.T ) {
543+ // Test Outlook-style email where PGP content is base64-encoded in an attachment
544+ // This is the actual format Microsoft/Outlook uses for PGP/MIME emails
545+ // The base64 decodes to: "-----BEGIN PGP MESSAGE-----\n\nhQEMAtest\n=xxxx\n-----END PGP MESSAGE-----\n"
546+ base64PGP := "LS0tLS1CRUdJTiBQR1AgTUVTU0FHRS0tLS0tCgpoUUVNQXRlc3QKPXh4eHgKLS0tLS1FTkQgUEdQIE1FU1NBR0UtLS0tLQo="
547+
548+ rawMIME := `From: sender@outlook.com
549+ To: recipient@example.com
550+ Content-Type: multipart/mixed;
551+ boundary="_003_OutlookBoundary_"
552+ MIME-Version: 1.0
553+
554+ --_003_OutlookBoundary_
555+ Content-Type: text/plain; charset="us-ascii"
556+ Content-Transfer-Encoding: quoted-printable
557+
558+
559+ --_003_OutlookBoundary_
560+ Content-Type: application/pgp-encrypted; name="PGPMIME version identification"
561+ Content-Description: PGP/MIME version identification
562+ Content-Disposition: attachment; filename="PGPMIME version identification"
563+ Content-Transfer-Encoding: base64
564+
565+ VmVyc2lvbjogMQ0K
566+
567+ --_003_OutlookBoundary_
568+ Content-Type: application/octet-stream; name="encrypted.asc"
569+ Content-Description: OpenPGP encrypted message.asc
570+ Content-Disposition: attachment; filename="encrypted.asc"
571+ Content-Transfer-Encoding: base64
572+
573+ ` + base64PGP + `
574+
575+ --_003_OutlookBoundary_--`
576+
577+ got := extractInlinePGP (rawMIME )
578+ if got == nil {
579+ t .Fatal ("extractInlinePGP() returned nil for base64-encoded Outlook attachment" )
580+ }
581+
582+ gotStr := string (got )
583+
584+ // Should extract the decoded PGP message
585+ if ! strings .HasPrefix (gotStr , "-----BEGIN PGP MESSAGE-----" ) {
586+ t .Errorf ("extractInlinePGP() should start with PGP header, got: %q" , gotStr )
587+ }
588+ if ! strings .HasSuffix (gotStr , "-----END PGP MESSAGE-----" ) {
589+ t .Errorf ("extractInlinePGP() should end with PGP footer, got: %q" , gotStr )
590+ }
591+ if ! strings .Contains (gotStr , "hQEMAtest" ) {
592+ t .Errorf ("extractInlinePGP() should contain the encrypted data, got: %q" , gotStr )
593+ }
594+ }
0 commit comments