1 /* mimemaker.c - Construct MIME message out of a MAPI
2 * Copyright (C) 2007, 2008 g10 Code GmbH
3 * Copyright (C) 2015 by Bundesamt für Sicherheit in der Informationstechnik
4 * Software engineering by Intevation GmbH
6 * This file is part of GpgOL.
8 * GpgOL is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU Lesser General Public
10 * License as published by the Free Software Foundation; either
11 * version 2.1 of the License, or (at your option) any later version.
13 * GpgOL is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU Lesser General Public License for more details.
18 * You should have received a copy of the GNU Lesser General Public License
19 * along with this program; if not, see <http://www.gnu.org/licenses/>.
38 #include "mymapitags.h"
42 #include "mimemaker.h"
47 #define _(a) utf8_gettext (a)
49 static const unsigned char oid_mimetag[] =
50 {0x2A, 0x86, 0x48, 0x86, 0xf7, 0x14, 0x03, 0x0a, 0x04};
52 /* The base-64 list used for base64 encoding. */
53 static unsigned char bintoasc[64+1] = ("ABCDEFGHIJKLMNOPQRSTUVWXYZ"
54 "abcdefghijklmnopqrstuvwxyz"
58 /* Object used to collect data in a memory buffer. */
61 size_t len; /* Used length. */
62 size_t size; /* Allocated length of BUF. */
63 char *buf; /* Malloced buffer. */
67 /*** local prototypes ***/
68 static int write_multistring (sink_t sink, const char *text1,
69 ...) GPGOL_GCC_A_SENTINEL(0);
75 /* Standard write method used with a sink_t object. */
77 sink_std_write (sink_t sink, const void *data, size_t datalen)
80 LPSTREAM stream = static_cast<LPSTREAM>(sink->cb_data);
84 log_error ("%s:%s: sink not setup for writing", SRCNAME, __func__);
88 return 0; /* Flush - nothing to do here. */
90 hr = stream->Write(data, datalen, NULL);
93 log_error ("%s:%s: Write failed: hr=%#lx", SRCNAME, __func__, hr);
100 sink_string_write (sink_t sink, const void *data, size_t datalen)
102 Mail *mail = static_cast<Mail *>(sink->cb_data);
103 mail->appendToInlineBody (std::string((char*)data, datalen));
107 /* Write method used with a sink_t that contains a file object. */
109 sink_file_write (sink_t sink, const void *data, size_t datalen)
111 HANDLE hFile = sink->cb_data;
114 if (!hFile || hFile == INVALID_HANDLE_VALUE)
116 log_error ("%s:%s: sink not setup for writing", SRCNAME, __func__);
120 return 0; /* Flush - nothing to do here. */
122 if (!WriteFile (hFile, data, datalen, &written, NULL))
124 log_error ("%s:%s: Write failed: ", SRCNAME, __func__);
131 /* Create a new MAPI attchment for MESSAGE which will be used to
132 prepare the MIME message. On sucess the stream to write the data
133 to is stored at STREAM and the attachment object itself is
134 returned. The caller needs to call SaveChanges. Returns NULL on
135 failure in which case STREAM will be set to NULL. */
137 create_mapi_attachment (LPMESSAGE message, sink_t sink,
138 const char *overrideMimeTag)
146 sink->cb_data = NULL;
147 sink->writefnc = NULL;
148 hr = message->CreateAttach(NULL, 0, &pos, &att);
152 log_error ("%s:%s: can't create attachment: hr=%#lx\n",
153 SRCNAME, __func__, hr);
157 prop.ulPropTag = PR_ATTACH_METHOD;
158 prop.Value.ul = ATTACH_BY_VALUE;
159 hr = HrSetOneProp ((LPMAPIPROP)att, &prop);
162 log_error ("%s:%s: can't set attach method: hr=%#lx\n",
163 SRCNAME, __func__, hr);
167 /* Mark that attachment so that we know why it has been created. */
168 if (get_gpgolattachtype_tag (message, &prop.ulPropTag) )
170 prop.Value.l = ATTACHTYPE_MOSSTEMPL;
171 hr = HrSetOneProp ((LPMAPIPROP)att, &prop);
174 log_error ("%s:%s: can't set %s property: hr=%#lx\n",
175 SRCNAME, __func__, "GpgOL Attach Type", hr);
180 /* We better insert a short filename. */
181 prop.ulPropTag = PR_ATTACH_FILENAME_A;
182 prop.Value.lpszA = xstrdup (MIMEATTACHFILENAME);
183 hr = HrSetOneProp ((LPMAPIPROP)att, &prop);
184 xfree (prop.Value.lpszA);
187 log_error ("%s:%s: can't set attach filename: hr=%#lx\n",
188 SRCNAME, __func__, hr);
193 /* Even for encrypted messages we need to set the MAPI property to
194 multipart/signed. This seems to be a part of the trigger which
195 leads OL to process such a message in a special way. */
196 prop.ulPropTag = PR_ATTACH_TAG;
197 prop.Value.bin.cb = sizeof oid_mimetag;
198 prop.Value.bin.lpb = (LPBYTE)oid_mimetag;
199 hr = HrSetOneProp ((LPMAPIPROP)att, &prop);
202 prop.ulPropTag = PR_ATTACH_MIME_TAG_A;
203 prop.Value.lpszA = overrideMimeTag ? xstrdup (overrideMimeTag) :
204 xstrdup ("multipart/signed");
207 log_debug ("%s:%s: using override mimetag: %s\n",
208 SRCNAME, __func__, overrideMimeTag);
210 hr = HrSetOneProp ((LPMAPIPROP)att, &prop);
211 xfree (prop.Value.lpszA);
215 log_error ("%s:%s: can't set attach mime tag: hr=%#lx\n",
216 SRCNAME, __func__, hr);
221 hr = gpgol_openProperty (att, PR_ATTACH_DATA_BIN, &IID_IStream, 0,
222 (MAPI_CREATE|MAPI_MODIFY), &punk);
225 log_error ("%s:%s: can't create output stream: hr=%#lx\n",
226 SRCNAME, __func__, hr);
229 sink->cb_data = (LPSTREAM)punk;
230 sink->writefnc = sink_std_write;
239 /* Write data to a sink_t. */
241 write_buffer (sink_t sink, const void *data, size_t datalen)
243 if (!sink || !sink->writefnc)
245 log_error ("%s:%s: sink not properly setup", SRCNAME, __func__);
248 return sink->writefnc (sink, data, datalen);
251 /* Same as above but used for passing as callback function. This
252 fucntion does not return an error code but the number of bytes
255 write_buffer_for_cb (void *opaque, const void *data, size_t datalen)
257 sink_t sink = (sink_t) opaque;
258 sink->enc_counter += datalen;
259 return write_buffer (sink, data, datalen) ? -1 : datalen;
263 /* Write the string TEXT to the IStream STREAM. Returns 0 on sucsess,
264 prints an error message and returns -1 on error. */
266 write_string (sink_t sink, const char *text)
268 return write_buffer (sink, text, strlen (text));
272 /* Write the string TEXT1 and all folloing arguments of type (const
273 char*) to the SINK. The list of argumens needs to be terminated
274 with a NULL. Returns 0 on sucsess, prints an error message and
275 returns -1 on error. */
277 write_multistring (sink_t sink, const char *text1, ...)
283 va_start (arg_ptr, text1);
286 rc = write_string (sink, s);
287 while (!rc && (s=va_arg (arg_ptr, const char *)));
294 /* Helper to write a boundary to the output sink. The leading LF
295 will be written as well. */
297 write_boundary (sink_t sink, const char *boundary, int lastone)
299 int rc = write_string (sink, "\r\n--");
301 rc = write_string (sink, boundary);
303 rc = write_string (sink, lastone? "--\r\n":"\r\n");
308 /* Write DATALEN bytes of DATA to SINK in base64 encoding. This
309 creates a complete Base64 chunk including the trailing fillers. */
311 write_b64 (sink_t sink, const void *data, size_t datalen)
314 const unsigned char *p;
315 unsigned char inbuf[4];
320 log_debug (" writing base64 of length %d\n", (int)datalen);
323 for (p = (const unsigned char*)data; datalen; p++, datalen--)
328 /* We need space for a quad and a possible CR,LF. */
329 if (outlen+4+2 >= sizeof outbuf)
331 if ((rc = write_buffer (sink, outbuf, outlen)))
335 outbuf[outlen++] = bintoasc[(*inbuf>>2)&077];
336 outbuf[outlen++] = bintoasc[(((*inbuf<<4)&060)
337 |((inbuf[1] >> 4)&017))&077];
338 outbuf[outlen++] = bintoasc[(((inbuf[1]<<2)&074)
339 |((inbuf[2]>>6)&03))&077];
340 outbuf[outlen++] = bintoasc[inbuf[2]&077];
342 if (++quads >= (64/4))
345 outbuf[outlen++] = '\r';
346 outbuf[outlen++] = '\n';
351 /* We need space for a quad and a final CR,LF. */
352 if (outlen+4+2 >= sizeof outbuf)
354 if ((rc = write_buffer (sink, outbuf, outlen)))
360 outbuf[outlen++] = bintoasc[(*inbuf>>2)&077];
363 outbuf[outlen++] = bintoasc[((*inbuf<<4)&060)&077];
364 outbuf[outlen++] = '=';
365 outbuf[outlen++] = '=';
369 outbuf[outlen++] = bintoasc[(((*inbuf<<4)&060)
370 |((inbuf[1]>>4)&017))&077];
371 outbuf[outlen++] = bintoasc[((inbuf[1]<<2)&074)&077];
372 outbuf[outlen++] = '=';
379 outbuf[outlen++] = '\r';
380 outbuf[outlen++] = '\n';
385 if ((rc = write_buffer (sink, outbuf, outlen)))
392 /* Write DATALEN bytes of DATA to SINK in quoted-prinable encoding. */
394 write_qp (sink_t sink, const void *data, size_t datalen)
397 const unsigned char *p;
398 char outbuf[80]; /* We only need 76 octect + 2 for the lineend. */
401 /* Check whether the current character is followed by a line ending.
402 Note that the end of the etxt also counts as a lineending */
403 #define nextlf_p() ((datalen > 2 && p[1] == '\r' && p[2] == '\n') \
404 || (datalen > 1 && p[1] == '\n') \
407 /* Macro to insert a soft line break if needed. */
408 # define do_softlf(n) \
410 if (outidx + (n) > 76 \
411 || (outidx + (n) == 76 && !nextlf_p())) \
413 outbuf[outidx++] = '='; \
414 outbuf[outidx++] = '\r'; \
415 outbuf[outidx++] = '\n'; \
416 if ((rc = write_buffer (sink, outbuf, outidx))) \
422 log_debug (" writing qp of length %d\n", (int)datalen);
424 for (p = (const unsigned char*) data; datalen; p++, datalen--)
426 if ((datalen > 1 && *p == '\r' && p[1] == '\n') || *p == '\n')
429 outbuf[outidx++] = '\r';
430 outbuf[outidx++] = '\n';
431 if ((rc = write_buffer (sink, outbuf, outidx)))
440 else if (*p == '\t' || *p == ' ')
442 /* Check whether tab or space is followed by a line break
443 which forbids verbatim encoding. If we are already at
444 the end of the buffer we take that as a line end too. */
448 outbuf[outidx++] = '=';
449 outbuf[outidx++] = tohex ((*p>>4)&15);
450 outbuf[outidx++] = tohex (*p&15);
455 outbuf[outidx++] = *p;
459 else if (!outidx && *p == '.' && nextlf_p () )
461 /* We better protect a line with just a single dot. */
462 outbuf[outidx++] = '=';
463 outbuf[outidx++] = tohex ((*p>>4)&15);
464 outbuf[outidx++] = tohex (*p&15);
466 else if (!outidx && datalen >= 5 && !memcmp (p, "From ", 5))
468 /* Protect the 'F' so that MTAs won't prefix the "From "
470 outbuf[outidx++] = '=';
471 outbuf[outidx++] = tohex ((*p>>4)&15);
472 outbuf[outidx++] = tohex (*p&15);
474 else if (*p >= '!' && *p <= '~' && *p != '=')
477 outbuf[outidx++] = *p;
482 outbuf[outidx++] = '=';
483 outbuf[outidx++] = tohex ((*p>>4)&15);
484 outbuf[outidx++] = tohex (*p&15);
489 outbuf[outidx++] = '\r';
490 outbuf[outidx++] = '\n';
491 if ((rc = write_buffer (sink, outbuf, outidx)))
501 /* Write DATALEN bytes of DATA to SINK in plain ascii encoding. */
503 write_plain (sink_t sink, const void *data, size_t datalen)
506 const unsigned char *p;
510 log_debug (" writing ascii of length %d\n", (int)datalen);
512 for (p = (const unsigned char*) data; datalen; p++, datalen--)
514 if ((datalen > 1 && *p == '\r' && p[1] == '\n') || *p == '\n')
516 outbuf[outidx++] = '\r';
517 outbuf[outidx++] = '\n';
518 if ((rc = write_buffer (sink, outbuf, outidx)))
527 else if (!outidx && *p == '.'
528 && ( (datalen > 2 && p[1] == '\r' && p[2] == '\n')
529 || (datalen > 1 && p[1] == '\n')
532 /* Better protect a line with just a single dot. We do
533 this by adding a space. */
534 outbuf[outidx++] = *p;
535 outbuf[outidx++] = ' ';
537 else if (outidx > 80)
539 /* We should never be called for too long lines - QP should
541 log_error ("%s:%s: BUG: line longer than exepcted",
546 outbuf[outidx++] = *p;
551 outbuf[outidx++] = '\r';
552 outbuf[outidx++] = '\n';
553 if ((rc = write_buffer (sink, outbuf, outidx)))
561 /* Infer the conent type from the FILENAME. The return value is
562 a static string there won't be an error return. In case Bae 64
563 encoding is required for the type true will be stored at FORCE_B64;
564 however, this is only a shortcut and if that is not set, the caller
565 should infer the encoding by other means. */
567 infer_content_type (const char * /*data*/, size_t /*datalen*/,
568 const char *filename, int is_mapibody, int *force_b64)
576 { 1, "3gp", "video/3gpp" },
577 { 1, "abw", "application/x-abiword" },
578 { 1, "ai", "application/postscript" },
579 { 1, "au", "audio/basic" },
580 { 1, "bin", "application/octet-stream" },
581 { 1, "class", "application/java-vm" },
582 { 1, "cpt", "application/mac-compactpro" },
583 { 0, "css", "text/css" },
584 { 0, "csv", "text/comma-separated-values" },
585 { 1, "deb", "application/x-debian-package" },
586 { 1, "dl", "video/dl" },
587 { 1, "doc", "application/msword" },
588 { 1, "docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" },
589 { 1, "dot", "application/msword" },
590 { 1, "dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template" },
591 { 1, "docm", "application/application/vnd.ms-word.document.macroEnabled.12" },
592 { 1, "dotm", "application/vnd.ms-word.template.macroEnabled.12" },
593 { 1, "dv", "video/dv" },
594 { 1, "dvi", "application/x-dvi" },
595 { 1, "eml", "message/rfc822" },
596 { 1, "eps", "application/postscript" },
597 { 1, "fig", "application/x-xfig" },
598 { 1, "flac", "application/x-flac" },
599 { 1, "fli", "video/fli" },
600 { 1, "gif", "image/gif" },
601 { 1, "gl", "video/gl" },
602 { 1, "gnumeric", "application/x-gnumeric" },
603 { 1, "hqx", "application/mac-binhex40" },
604 { 1, "hta", "application/hta" },
605 { 0, "htm", "text/html" },
606 { 0, "html", "text/html" },
607 { 0, "ics", "text/calendar" },
608 { 1, "jar", "application/java-archive" },
609 { 1, "jpeg", "image/jpeg" },
610 { 1, "jpg", "image/jpeg" },
611 { 1, "js", "application/x-javascript" },
612 { 1, "latex", "application/x-latex" },
613 { 1, "lha", "application/x-lha" },
614 { 1, "lzh", "application/x-lzh" },
615 { 1, "lzx", "application/x-lzx" },
616 { 1, "m3u", "audio/mpegurl" },
617 { 1, "m4a", "audio/mpeg" },
618 { 1, "mdb", "application/msaccess" },
619 { 1, "midi", "audio/midi" },
620 { 1, "mov", "video/quicktime" },
621 { 1, "mp2", "audio/mpeg" },
622 { 1, "mp3", "audio/mpeg" },
623 { 1, "mp4", "video/mp4" },
624 { 1, "mpeg", "video/mpeg" },
625 { 1, "mpega", "audio/mpeg" },
626 { 1, "mpg", "video/mpeg" },
627 { 1, "mpga", "audio/mpeg" },
628 { 1, "msi", "application/x-msi" },
629 { 1, "mxu", "video/vnd.mpegurl" },
630 { 1, "nb", "application/mathematica" },
631 { 1, "oda", "application/oda" },
632 { 1, "odb", "application/vnd.oasis.opendocument.database" },
633 { 1, "odc", "application/vnd.oasis.opendocument.chart" },
634 { 1, "odf", "application/vnd.oasis.opendocument.formula" },
635 { 1, "odg", "application/vnd.oasis.opendocument.graphics" },
636 { 1, "odi", "application/vnd.oasis.opendocument.image" },
637 { 1, "odm", "application/vnd.oasis.opendocument.text-master" },
638 { 1, "odp", "application/vnd.oasis.opendocument.presentation" },
639 { 1, "ods", "application/vnd.oasis.opendocument.spreadsheet" },
640 { 1, "odt", "application/vnd.oasis.opendocument.text" },
641 { 1, "ogg", "application/ogg" },
642 { 1, "otg", "application/vnd.oasis.opendocument.graphics-template" },
643 { 1, "oth", "application/vnd.oasis.opendocument.text-web" },
644 { 1, "otp", "application/vnd.oasis.opendocument.presentation-template"},
645 { 1, "ots", "application/vnd.oasis.opendocument.spreadsheet-template"},
646 { 1, "ott", "application/vnd.oasis.opendocument.text-template" },
647 { 1, "pdf", "application/pdf" },
648 { 1, "png", "image/png" },
649 { 1, "pps", "application/vnd.ms-powerpoint" },
650 { 1, "ppt", "application/vnd.ms-powerpoint" },
651 { 1, "pot", "application/vnd.ms-powerpoint" },
652 { 1, "ppa", "application/vnd.ms-powerpoint" },
653 { 1, "pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" },
654 { 1, "potx", "application/vnd.openxmlformats-officedocument.presentationml.template" },
655 { 1, "ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow" },
656 { 1, "ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12" },
657 { 1, "pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12" },
658 { 1, "potm", "application/vnd.ms-powerpoint.template.macroEnabled.12" },
659 { 1, "ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12" },
660 { 1, "prf", "application/pics-rules" },
661 { 1, "ps", "application/postscript" },
662 { 1, "qt", "video/quicktime" },
663 { 1, "rar", "application/rar" },
664 { 1, "rdf", "application/rdf+xml" },
665 { 1, "rpm", "application/x-redhat-package-manager" },
666 { 0, "rss", "application/rss+xml" },
667 { 1, "ser", "application/java-serialized-object" },
668 { 0, "sh", "application/x-sh" },
669 { 0, "shtml", "text/html" },
670 { 1, "sid", "audio/prs.sid" },
671 { 0, "smil", "application/smil" },
672 { 1, "snd", "audio/basic" },
673 { 0, "svg", "image/svg+xml" },
674 { 1, "tar", "application/x-tar" },
675 { 0, "texi", "application/x-texinfo" },
676 { 0, "texinfo", "application/x-texinfo" },
677 { 1, "tif", "image/tiff" },
678 { 1, "tiff", "image/tiff" },
679 { 1, "torrent", "application/x-bittorrent" },
680 { 1, "tsp", "application/dsptype" },
681 { 0, "vrml", "model/vrml" },
682 { 1, "vsd", "application/vnd.visio" },
683 { 1, "wp5", "application/wordperfect5.1" },
684 { 1, "wpd", "application/wordperfect" },
685 { 0, "xhtml", "application/xhtml+xml" },
686 { 1, "xlb", "application/vnd.ms-excel" },
687 { 1, "xls", "application/vnd.ms-excel" },
688 { 1, "xlsx", "application/vnd.ms-excel" },
689 { 1, "xlt", "application/vnd.ms-excel" },
690 { 1, "xla", "application/vnd.ms-excel" },
691 { 1, "xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template" },
692 { 1, "xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12" },
693 { 1, "xltm", "application/vnd.ms-excel.template.macroEnabled.12" },
694 { 1, "xlam", "application/vnd.ms-excel.addin.macroEnabled.12" },
695 { 1, "xlsb", "application/application/vnd.ms-excel.sheet.binary.macroEnabled.12" },
696 { 0, "xml", "application/xml" },
697 { 0, "xsl", "application/xml" },
698 { 0, "xul", "application/vnd.mozilla.xul+xml" },
699 { 1, "zip", "application/zip" },
708 const char *dot = strrchr (filename, '.');
716 /* Check for at least one char after the dot. */
717 if (suffix.size() > 1)
721 std::transform(suffix.begin(), suffix.end(), suffix.begin(), ::tolower);
722 for (i=0; suffix_table[i].suffix; i++)
724 if (!strcmp (suffix_table[i].suffix, suffix.c_str()))
726 if (suffix_table[i].b64)
728 return suffix_table[i].ct;
733 /* Not found via filename, look at the content. */
735 if (is_mapibody == 1)
739 else if (is_mapibody == 2)
743 return "application/octet-stream";
746 /* Figure out the best encoding to be used for the part. Return values are
751 infer_content_encoding (const void *data, size_t datalen)
753 const unsigned char *p;
755 size_t len, maxlen, highbin, lowbin, ntotal;
758 len = maxlen = lowbin = highbin = 0;
760 for (p = (const unsigned char*) data; datalen; p++, datalen--)
765 else if ((datalen > 1 && *p == '\r' && p[1] == '\n') || *p == '\n')
774 /* CR not followed by a linefeed. */
777 else if (*p == '\t' || *p == ' ' || *p == '\f')
779 else if (*p < ' ' || *p == 127)
781 else if (len == 1 && datalen > 2
782 && *p == '-' && p[1] == '-' && p[2] == ' '
783 && ( (datalen > 4 && p[3] == '\r' && p[4] == '\n')
784 || (datalen > 3 && p[3] == '\n')
787 /* This is a "-- \r\n" line, thus it indicates the usual
788 signature line delimiter. We need to protect the
792 else if (len == 1 && datalen > 5 && !memcmp (p, "--=-=", 5))
794 /* This look pretty much like a our own boundary.
795 We better protect it by forcing QP encoding. */
798 else if (len == 1 && datalen >= 5 && !memcmp (p, "From ", 5))
800 /* The usual From hack is required so that MTAs do not
801 prefix it with an '>'. */
808 if (maxlen <= 76 && !lowbin && !highbin && !need_qp)
809 return 0; /* Plain ASCII is sufficient. */
811 /* Somewhere in the Outlook documentation 20% is mentioned as
812 discriminating value for Base64. Though our counting won't be
813 identical we use that value to behave closely to it. */
814 if (ntotal && ((float)(lowbin+highbin))/ntotal < 0.20)
815 return 1; /* Use quoted printable. */
817 return 2; /* Use base64. */
821 /* Convert an utf8 input string to RFC2047 base64 encoding which
822 is the subset of RFC2047 outlook likes.
823 Return value needs to be freed.
826 utf8_to_rfc2047b (const char *input)
830 int inferred_encoding = 0;
835 inferred_encoding = infer_content_encoding (input, strlen (input));
836 if (!inferred_encoding)
838 return xstrdup (input);
840 log_debug ("%s:%s: Encoding attachment filename. With: %s ",
841 SRCNAME, __func__, inferred_encoding == 2 ? "Base64" : "QP");
843 if (inferred_encoding == 2)
845 encoded = b64_encode (input, strlen (input));
846 if (gpgrt_asprintf (&ret, "=?utf-8?B?%s?=", encoded) == -1)
848 log_error ("%s:%s: Error: %i", SRCNAME, __func__, __LINE__);
855 /* There is a Bug here. If you encode 4 Byte UTF-8 outlook can't
856 handle it itself. And sends out a message with ?? inserted in
857 that place. This triggers an invalid signature. */
858 encoded = qp_encode (input, strlen (input), NULL);
859 if (gpgrt_asprintf (&ret, "=?utf-8?Q?%s?=", encoded) == -1)
861 log_error ("%s:%s: Error: %i", SRCNAME, __func__, __LINE__);
870 /* Write a MIME part to SINK. First the BOUNDARY is written (unless
871 it is NULL) then the DATA is analyzed and appropriate headers are
872 written. If FILENAME is given it will be added to the part's
873 header. IS_MAPIBODY should be passed as true if the data has been
874 retrieved from the body property. */
876 write_part (sink_t sink, const char *data, size_t datalen,
877 const char *boundary, const char *filename, int is_mapibody,
878 const char *content_id = NULL)
882 int use_b64, use_qp, is_text;
883 char *encoded_filename;
887 /* If there is a filename strip the directory part. Take care
888 that there might be slashes or backslashes. */
889 const char *s1 = strrchr (filename, '/');
890 const char *s2 = strrchr (filename, '\\');
894 else if (s1 && s2 && s2 > s1)
899 if (*filename && filename[1] == ':')
905 log_debug ("Writing part of length %d%s filename=`%s'\n",
906 (int)datalen, is_mapibody? " (body)":"",
907 filename ? anonstr (filename) : "[none]");
909 ct = infer_content_type (data, datalen, filename, is_mapibody, &use_b64);
913 switch (infer_content_encoding (data, datalen))
916 case 1: use_qp = 1; break;
917 default: use_b64 = 1; break;
920 is_text = !strncmp (ct, "text/", 5);
923 if ((rc = write_boundary (sink, boundary, 0)))
925 if ((rc=write_multistring (sink,
926 "Content-Type: ", ct,
927 (is_text || filename? ";\r\n" :"\r\n"),
931 /* OL inserts a charset parameter in many cases, so we do it right
932 away for all text parts. We can assume us-ascii if no special
933 encoding is required. */
935 if ((rc=write_multistring (sink,
937 (!use_qp && !use_b64? "us-ascii" : "utf-8"),
938 filename ? "\";\r\n" : "\"\r\n",
942 encoded_filename = utf8_to_rfc2047b (filename);
943 if (encoded_filename)
944 if ((rc=write_multistring (sink,
945 "\tname=\"", encoded_filename, "\"\r\n",
949 /* Note that we need to output even 7bit because OL inserts that
951 if ((rc = write_multistring (sink,
952 "Content-Transfer-Encoding: ",
953 (use_b64? "base64\r\n":
954 use_qp? "quoted-printable\r\n":"7bit\r\n"),
960 if ((rc=write_multistring (sink,
961 "Content-ID: <", content_id, ">\r\n",
965 else if (encoded_filename)
966 if ((rc=write_multistring (sink,
967 "Content-Disposition: attachment;\r\n"
968 "\tfilename=\"", encoded_filename, "\"\r\n",
972 xfree(encoded_filename);
974 /* Write delimiter. */
975 if ((rc = write_string (sink, "\r\n")))
978 /* Write the content. */
980 rc = write_b64 (sink, data, datalen);
982 rc = write_qp (sink, data, datalen);
984 rc = write_plain (sink, data, datalen);
990 /* Return the number of attachments in TABLE to be put into the MIME
993 count_usable_attachments (mapi_attach_item_t *table)
998 for (idx=0; !table[idx].end_of_table; idx++)
999 if (table[idx].attach_type == ATTACHTYPE_UNKNOWN
1000 && (table[idx].method == ATTACH_BY_VALUE
1001 || table[idx].method == ATTACH_OLE
1002 || table[idx].method == ATTACH_EMBEDDED_MSG))
1004 /* OLE and embedded are usable becase we plan to
1005 add support later. First version only handled
1006 them with a warning in write attachments. */
1012 /* Write out all attachments from TABLE separated by BOUNDARY to SINK.
1013 This function needs to be syncronized with count_usable_attachments.
1014 If only_related is 1 only include attachments for multipart/related they
1015 are excluded otherwise.
1016 If only_related is 2 all attachments are included regardless of
1019 write_attachments (sink_t sink,
1020 LPMESSAGE message, mapi_attach_item_t *table,
1021 const char *boundary, int only_related)
1026 bool warning_shown = false;
1029 for (idx=0; !table[idx].end_of_table; idx++)
1031 if (table[idx].attach_type == ATTACHTYPE_UNKNOWN
1032 && table[idx].method == ATTACH_BY_VALUE)
1034 if (only_related == 1 && !table[idx].content_id)
1038 else if (!only_related && table[idx].content_id)
1042 buffer = mapi_get_attach (message, table+idx, &buflen);
1044 log_debug ("Attachment at index %d not found\n", idx);
1046 log_debug ("Attachment at index %d: length=%d\n", idx, (int)buflen);
1049 rc = write_part (sink, buffer, buflen, boundary,
1050 table[idx].filename, 0, table[idx].content_id);
1053 log_error ("Write part returned err: %i", rc);
1057 else if (!only_related && !warning_shown
1058 && table[idx].attach_type == ATTACHTYPE_UNKNOWN
1059 && (table[idx].method == ATTACH_OLE
1060 || table[idx].method == ATTACH_EMBEDDED_MSG))
1063 log_debug ("%s:%s: detected OLE attachment. Showing warning.",
1065 gpgrt_asprintf (&fmt, _("The attachment '%s' is an Outlook item "
1066 "which is currently unsupported in crypto mails."),
1067 table[idx].filename ?
1068 table[idx].filename : _("Unknown"));
1069 std::string msg = fmt;
1073 gpgrt_asprintf (&fmt, _("Please encrypt '%s' with Kleopatra "
1074 "and attach it as a file."),
1075 table[idx].filename ?
1076 table[idx].filename : _("Unknown"));
1081 msg += _("Send anyway?");
1082 warning_shown = true;
1084 if (gpgol_message_box (get_active_hwnd (),
1086 _("Sorry, that's not possible, yet"),
1087 MB_APPLMODAL | MB_YESNO) == IDNO)
1094 log_debug ("%s:%s: Skipping unknown attachment at idx: %d type: %d"
1096 SRCNAME, __func__, idx, table[idx].attach_type,
1103 /* Returns 1 if all attachments are related. 2 if there is a
1104 related and a mixed attachment. 0 if there are no other parts*/
1106 is_related (Mail *mail, mapi_attach_item_t *table)
1108 if (!mail || !mail->isHTMLAlternative () || !table)
1115 for (int idx = 0; !table[idx].end_of_table; idx++)
1117 if (table[idx].content_id)
1126 return mixed + related;
1130 /* Delete all attachments from TABLE except for the one we just created */
1132 delete_all_attachments (LPMESSAGE message, mapi_attach_item_t *table)
1138 for (idx=0; !table[idx].end_of_table; idx++)
1140 if (table[idx].attach_type == ATTACHTYPE_MOSSTEMPL
1141 && table[idx].filename
1142 && !strcmp (table[idx].filename, MIMEATTACHFILENAME))
1144 hr = message->DeleteAttach (table[idx].mapipos, 0, NULL, 0);
1147 log_error ("%s:%s: DeleteAttach failed: hr=%#lx\n",
1148 SRCNAME, __func__, hr);
1156 /* Commit changes to the attachment ATTACH and release the object.
1157 SINK needs to be passed as well and will also be closed. Note that
1158 the address of ATTACH is expected so that the fucntion can set it
1161 close_mapi_attachment (LPATTACH *attach, sink_t sink)
1164 LPSTREAM stream = sink ? (LPSTREAM) sink->cb_data : NULL;
1168 log_error ("%s:%s: sink not setup", SRCNAME, __func__);
1171 hr = stream->Commit (0);
1174 log_error ("%s:%s: Commiting output stream failed: hr=%#lx",
1175 SRCNAME, __func__, hr);
1178 gpgol_release (stream);
1179 sink->cb_data = NULL;
1180 hr = (*attach)->SaveChanges (0);
1183 log_error ("%s:%s: SaveChanges of the attachment failed: hr=%#lx\n",
1184 SRCNAME, __func__, hr);
1187 gpgol_release ((*attach));
1193 /* Cancel changes to the attachment ATTACH and release the object.
1194 SINK needs to be passed as well and will also be closed. Note that
1195 the address of ATTACH is expected so that the fucntion can set it
1198 cancel_mapi_attachment (LPATTACH *attach, sink_t sink)
1200 LPSTREAM stream = sink ? (LPSTREAM) sink->cb_data : NULL;
1205 gpgol_release (stream);
1206 sink->cb_data = NULL;
1210 /* Fixme: Should we try to delete it or is there a Revert method? */
1211 gpgol_release ((*attach));
1218 /* Do the final processing for a message. */
1220 finalize_message (LPMESSAGE message, mapi_attach_item_t *att_table,
1221 protocol_t protocol, int encrypt, bool is_inline,
1222 bool is_draft, int exchange_major_version)
1226 SPropTagArray proparray;
1228 /* Set the message class. */
1229 prop.ulPropTag = PR_MESSAGE_CLASS_A;
1230 if (protocol == PROTOCOL_SMIME)
1232 /* When sending over exchange to the same server the recipient
1233 might see the message class we set here. So for S/MIME
1234 we keep the original. This makes the sent folder icon
1235 not immediately showing the GpgOL icon but gives other
1236 clients that do not have GpgOL installed a better chance
1237 to handle the mail. */
1238 if (encrypt && exchange_major_version >= 15)
1240 /* This only appears to work with later exchange versions */
1241 prop.Value.lpszA = xstrdup ("IPM.Note.SMIME");
1245 prop.Value.lpszA = xstrdup ("IPM.Note.SMIME.MultipartSigned");
1250 prop.Value.lpszA = xstrdup ("IPM.Note.InfoPathForm.GpgOL.SMIME.MultipartSigned");
1254 prop.Value.lpszA = xstrdup ("IPM.Note.InfoPathForm.GpgOLS.SMIME.MultipartSigned");
1259 /* For inline we stick with IPM.Note because Exchange Online would
1260 error out if we tried our S/MIME conversion trick with a text
1262 hr = message->SetProps(1, &prop, NULL);
1264 xfree(prop.Value.lpszA);
1267 log_error ("%s:%s: error setting the message class: hr=%#lx\n",
1268 SRCNAME, __func__, hr);
1272 /* We also need to set the message class into our custom
1273 property. This override is at least required for encrypted
1275 if (is_inline && mapi_set_gpgol_msg_class (message,
1277 (protocol == PROTOCOL_SMIME?
1278 "IPM.Note.GpgOL.OpaqueEncrypted" :
1279 "IPM.Note.GpgOL.PGPMessage") :
1280 "IPM.Note.GpgOL.ClearSigned")))
1282 log_error ("%s:%s: error setting gpgol msgclass",
1286 if (!is_inline && mapi_set_gpgol_msg_class (message,
1288 (protocol == PROTOCOL_SMIME?
1289 "IPM.Note.GpgOL.OpaqueEncrypted" :
1290 "IPM.Note.GpgOL.MultipartEncrypted") :
1291 "IPM.Note.GpgOL.MultipartSigned")))
1293 log_error ("%s:%s: error setting gpgol msgclass",
1298 proparray.cValues = 1;
1299 proparray.aulPropTag[0] = PR_BODY;
1300 hr = message->DeleteProps (&proparray, NULL);
1303 log_debug_w32 (hr, "%s:%s: deleting PR_BODY failed",
1307 proparray.cValues = 1;
1308 proparray.aulPropTag[0] = PR_BODY_HTML;
1309 hr = message->DeleteProps (&proparray, NULL);
1312 log_debug_w32 (hr, "%s:%s: deleting PR_BODY_HTML failed",
1316 /* Now delete all parts of the MAPI message except for the one
1317 attachment we just created. */
1318 if (delete_all_attachments (message, att_table))
1320 log_error ("%s:%s: error deleting attachments",
1325 /* Remove the draft info so that we don't leak the information on
1326 whether the message has been signed etc. when we send it.
1327 If it is a draft we are encrypting we want to keep them.
1329 To avoid confusion: draft_info for us means the state of
1330 the secure toggle button.
1334 mapi_set_gpgol_draft_info (message, NULL);
1337 if (mapi_save_changes (message, KEEP_OPEN_READWRITE|FORCE_SAVE))
1339 log_error ("%s:%s: error saving changes.",
1347 /* Helper to create the signing header. This includes enough space
1348 for later fixup of the micalg parameter. The MIME version is only
1349 written if FIRST is set. */
1351 create_top_signing_header (char *buffer, size_t buflen, protocol_t protocol,
1352 int first, const char *boundary, const char *micalg)
1354 snprintf (buffer, buflen,
1356 "Content-Type: multipart/signed;\r\n"
1357 "\tprotocol=\"application/%s\";\r\n"
1358 "\tmicalg=%-15.15s;\r\n"
1359 "\tboundary=\"%s\"\r\n"
1361 first? "MIME-Version: 1.0\r\n":"",
1362 (protocol==PROTOCOL_OPENPGP? "pgp-signature":"pkcs7-signature"),
1366 /* Add the body, either as multipart/alternative or just as the
1367 simple body part. Depending on the format set in outlook. To
1368 avoid memory duplication it takes the plain body as parameter.
1370 Boundary is the potential outer boundary of a multipart/mixed
1371 mail. If it is null we assume the multipart/alternative is
1374 return is zero on success.
1377 add_body (Mail *mail, const char *boundary, sink_t sink,
1378 const char *plain_body)
1384 bool is_alternative = false;
1387 is_alternative = mail->isHTMLAlternative ();
1391 if (!is_alternative || !plain_body)
1395 rc = write_part (sink, plain_body, strlen (plain_body),
1398 /* Just the plain body or no body. We are done. */
1402 /* Add a new multipart / mixed element. */
1403 if (boundary && write_boundary (sink, boundary, 0))
1409 /* Now for the multipart/alternative part. We never do HTML only. */
1410 char alt_boundary [BOUNDARYSIZE+1];
1411 generate_boundary (alt_boundary);
1413 if ((rc=write_multistring (sink,
1414 "Content-Type: multipart/alternative;\r\n",
1415 "\tboundary=\"", alt_boundary, "\"\r\n",
1416 "\r\n", /* <-- extra line */
1423 /* Now the plain body part */
1424 if ((rc = write_part (sink, plain_body, strlen (plain_body),
1425 alt_boundary, NULL, 1)))
1431 /* Now the html body. It is somehow not accessible through PR_HTML,
1432 OutlookSpy also shows MAPI Unsupported (but shows the data) strange.
1433 We just cache it. Memory is cheap :-) */
1434 char *html_body = mail->takeCachedHTMLBody ();
1437 log_error ("%s:%s: BUG: Body but no html body in alternative mail?",
1442 rc = write_part (sink, html_body, strlen (html_body),
1443 alt_boundary, NULL, 2);
1450 /* Finish our multipart */
1451 return write_boundary (sink, alt_boundary, 1);
1454 /* Add the body and attachments. Does multipart handling. */
1456 add_body_and_attachments (sink_t sink, LPMESSAGE message,
1457 mapi_attach_item_t *att_table, Mail *mail,
1458 const char *body, int n_att_usable)
1460 int related = is_related (mail, att_table);
1462 char inner_boundary[BOUNDARYSIZE+1];
1463 char outer_boundary[BOUNDARYSIZE+1];
1464 *outer_boundary = 0;
1465 *inner_boundary = 0;
1467 if (((body && n_att_usable) || n_att_usable > 1) && related == 1)
1469 /* A body and at least one attachment or more than one attachment */
1470 generate_boundary (outer_boundary);
1471 if ((rc=write_multistring (sink,
1472 "Content-Type: multipart/related;\r\n",
1473 "\tboundary=\"", outer_boundary, "\"\r\n",
1474 "\r\n", /* <--- Outlook adds an extra line. */
1478 else if ((body && n_att_usable) || n_att_usable > 1)
1480 generate_boundary (outer_boundary);
1481 if ((rc=write_multistring (sink,
1482 "Content-Type: multipart/mixed;\r\n",
1483 "\tboundary=\"", outer_boundary, "\"\r\n",
1484 "\r\n", /* <--- Outlook adds an extra line. */
1489 /* Only one part. */
1490 if (*outer_boundary && related == 2)
1492 /* We have attachments that are related to the body and unrelated
1493 attachments. So we need another part. */
1494 if ((rc=write_boundary (sink, outer_boundary, 0)))
1498 generate_boundary (inner_boundary);
1499 if ((rc=write_multistring (sink,
1500 "Content-Type: multipart/related;\r\n",
1501 "\tboundary=\"", inner_boundary, "\"\r\n",
1502 "\r\n", /* <--- Outlook adds an extra line. */
1510 if ((rc=add_body (mail, *inner_boundary ? inner_boundary :
1511 *outer_boundary ? outer_boundary : NULL,
1514 log_error ("%s:%s: Adding the body failed.",
1518 if (!rc && n_att_usable && related)
1520 /* Write the related attachments. */
1521 rc = write_attachments (sink, message, att_table,
1522 *inner_boundary? inner_boundary :
1523 *outer_boundary? outer_boundary : NULL, 1);
1528 /* Close the related part if neccessary.*/
1529 if (*inner_boundary && (rc=write_boundary (sink, inner_boundary, 1)))
1535 /* Now write the other attachments.
1537 If we are multipart related the related attachments were already
1538 written above. If we are not related we pass 2 to the write_attachements
1539 function to force that even attachments with a content id are written
1542 This happens for example when forwarding a plain text mail with
1545 if (!rc && n_att_usable)
1547 rc = write_attachments (sink, message, att_table,
1548 *outer_boundary? outer_boundary : NULL,
1556 /* Finish the possible multipart/mixed. */
1557 if (*outer_boundary && (rc = write_boundary (sink, outer_boundary, 1)))
1564 /* Helper from mime_encrypt. BOUNDARY is a buffer of at least
1565 BOUNDARYSIZE+1 bytes which will be set on return from that
1568 create_top_encryption_header (sink_t sink, protocol_t protocol, char *boundary,
1569 bool is_inline, int exchange_major_version)
1577 /* This would be nice and worked for Google Sync but it failed
1578 for Microsoft Exchange Online *sigh* so we put the body
1579 instead into the oom body property and stick with IPM Note.
1580 rc = write_multistring (sink,
1581 "MIME-Version: 1.0\r\n"
1582 "Content-Type: text/plain;\r\n"
1583 "\tcharset=\"iso-8859-1\"\r\n"
1584 "Content-Transfer-Encoding: 7BIT\r\n"
1589 else if (protocol == PROTOCOL_SMIME)
1592 if (exchange_major_version >= 15)
1595 For S/MIME encrypted mails we do not use the S/MIME conversion
1596 code anymore. With Exchange 2016 this no longer works. Instead
1597 we set an override mime tag, the extended headers in OOM in
1598 Mail::update_crypt_oom and let outlook convert the attachment
1601 A bit more details can be found in T3853 / T3884
1607 rc = write_multistring (sink,
1608 "Content-Type: application/pkcs7-mime; "
1609 "smime-type=enveloped-data;\r\n"
1610 "\tname=\"smime.p7m\"\r\n"
1611 "Content-Disposition: attachment; filename=\"smime.p7m\"\r\n"
1612 "Content-Transfer-Encoding: base64\r\n"
1613 "MIME-Version: 1.0\r\n"
1620 generate_boundary (boundary);
1621 rc = write_multistring (sink,
1622 "MIME-Version: 1.0\r\n"
1623 "Content-Type: multipart/encrypted;\r\n"
1624 "\tprotocol=\"application/pgp-encrypted\";\r\n",
1625 "\tboundary=\"", boundary, "\"\r\n",
1630 /* Write the PGP/MIME encrypted part. */
1631 rc = write_boundary (sink, boundary, 0);
1634 rc = write_multistring (sink,
1635 "Content-Type: application/pgp-encrypted\r\n"
1637 "Version: 1\r\n", NULL);
1641 /* And start the second part. */
1642 rc = write_boundary (sink, boundary, 0);
1645 rc = write_multistring (sink,
1646 "Content-Type: application/octet-stream\r\n"
1647 "Content-Disposition: inline;\r\n"
1648 "\tfilename=\"" OPENPGP_ENC_NAME "\"\r\n"
1649 "Content-Transfer-Encoding: 7Bit\r\n"
1658 restore_msg_from_moss (LPMESSAGE message, LPDISPATCH moss_att,
1659 msgtype_t type, char *msgcls)
1661 struct sink_s sinkmem;
1662 sink_t sink = &sinkmem;
1665 char boundary[BOUNDARYSIZE+1];
1669 LPATTACH new_attach = create_mapi_attachment (message,
1671 log_debug ("Restore message from moss called.");
1674 log_error ("%s:%s: Error: %i", SRCNAME, __func__, __LINE__);
1678 if (type == MSGTYPE_SMIME)
1680 create_top_encryption_header (sink, PROTOCOL_SMIME, boundary);
1682 else if (type == MSGTYPE_GPGOL_MULTIPART_ENCRYPTED)
1684 create_top_encryption_header (sink, PROTOCOL_OPENPGP, boundary);
1688 log_error ("%s:%s: Unsupported messagetype: %i",
1689 SRCNAME, __func__, type);
1693 orig = get_pa_string (moss_att, PR_ATTACH_DATA_BIN_DASL);
1697 log_error ("%s:%s: Error: %i", SRCNAME, __func__, __LINE__);
1701 if (write_string (sink, orig))
1703 log_error ("%s:%s: Error: %i", SRCNAME, __func__, __LINE__);
1707 if (*boundary && write_boundary (sink, boundary, 1))
1709 log_error ("%s:%s: Error: %i", SRCNAME, __func__, __LINE__);
1713 if (close_mapi_attachment (&new_attach, sink))
1715 log_error ("%s:%s: Error: %i", SRCNAME, __func__, __LINE__);