MsgConverter.cs
author Thomas
Fri, 24 Aug 2018 16:20:59 +0200
changeset 2347 2842144ab3ac
parent 1841 476750890251
child 2378 95c2c791974d
permissions -rw-r--r--
Update version to 1.0.114
     1 
     2 namespace pEp
     3 {
     4     using System;
     5     using System.Collections.Generic;
     6     using System.Diagnostics;
     7     using System.IO;
     8     using System.Linq;
     9     using System.Runtime.InteropServices;
    10     using System.Runtime.InteropServices.ComTypes;
    11     using System.Text;
    12     using Outlook = Microsoft.Office.Interop.Outlook;
    13 
    14     /// <summary>
    15     /// Helper class to convert attached Messages to standard, internet-readable formats.
    16     /// </summary>
    17     internal static class MsgConverter
    18     {
    19         public static Guid CLSID_IConverterSession = new Guid("{4e3a7680-b77a-11d0-9da5-00c04fd65685}");
    20 
    21         public static IConverterSession CreateConverterSession()
    22         {
    23             Type converter = Type.GetTypeFromCLSID(CLSID_IConverterSession);
    24             object obj = Activator.CreateInstance(converter);
    25             var session = (IConverterSession)obj;
    26             session.SetTextWrapping(false, 0); // We do not want to wrap any texts...
    27 
    28             return session;
    29         }
    30 
    31         public static byte[] TryConvertEmbeddedItemToOpenFormat(Outlook.Attachment attachment, ref string fileName, ref string mimeType)
    32         {
    33             // Convert an attached embedded item (e. G. "forward as attachment") to an
    34             // open standard byte stream if possible.
    35             // TODO: Possible improvements:
    36             // - Try to get the pointer to the embedded item via MAPI without using a temporary file.
    37             // - Try to create the stream as close as possible as the original stream, possibly using PR_TRANSPORT_MESSAGE_HEADERS and other hints.
    38             // - Try to convert non-message types, e. G. create an ICS out of appointments
    39             // - When all receipients are guaranteed to be Outlook users, maybe we can just return the .msg as is. (This might have other implications...)
    40 
    41             Debug.Assert(attachment.Type == Outlook.OlAttachmentType.olEmbeddeditem, "wrong attachment type" + attachment.Type);
    42 
    43             var tempFile = Path.Combine(Path.GetTempPath(), "pep" + Guid.NewGuid().ToString() + ".msg");
    44 
    45             try
    46             {
    47                 attachment.SaveAsFile(tempFile);
    48 
    49                 object asMail = Globals.ThisAddIn.Application.Session.OpenSharedItem(tempFile);
    50                 try
    51                 {
    52                     var omi = (Outlook.MailItem)asMail;
    53                     try
    54                     {
    55                         var myStream = new MemoryStreamComMinimalWrapper();
    56                         IStream iStream = myStream;
    57 
    58                         // As this MAPIOBJECT is internally used by outlook, we should not free it,
    59                         // to prevent problems with internal Outlook handling (the reference we get
    60                         // might be a shared reference...)
    61                         object mapiObject = omi.MAPIOBJECT;
    62                         if (mapiObject == null)
    63                         {
    64                             Debug.Print("No MAPI Object found, using unconverted attachment data.");
    65                             return File.ReadAllBytes(tempFile);
    66                         }
    67 
    68                         var mapiMessage = (IMessage)mapiObject;
    69 
    70                         var session = CreateConverterSession();
    71                         try
    72                         {
    73                             // TODO: Possibly check whether the original message had any MIME headers
    74                             // using the PR_TRANSPORT_MESSAGE_HEADERS property, and call 
    75                             // Session.SetSaveFormat(MimeSaveType.SAVE_RFC_1521) or Session.SetSaveFormat(MimeSaveType.Save_RFC822)
    76                             // depending on whether the original message was a MIME message or not.
    77                             var hr = session.MAPIToMIMEStm(mapiMessage, iStream, CCSF.SMTP | CCSF.EightBITHEADERS);
    78 
    79                             Marshal.ThrowExceptionForHR(hr);
    80                         }
    81                         finally
    82                         {
    83                             // This com object is locally scoped, noone else has a reference, so we release it to clean up ressources.
    84                             Marshal.FinalReleaseComObject(session);
    85                         }
    86 
    87                         var bytes = myStream.ToArray();
    88 
    89                         fileName = Path.ChangeExtension(fileName ?? attachment.FileName, ".eml");
    90                         mimeType = "message/rfc822"; // This will only help with gpg/mime or s/mime, where the mime type can be retained.
    91                         return bytes;
    92                     }
    93                     finally
    94                     {
    95                         // This com objects are locally scoped, and noone else has a reference,
    96                         // so we release them to clean up ressources.
    97                         Marshal.FinalReleaseComObject(omi);
    98                     }
    99                 }
   100                 finally
   101                 {
   102                     // This com objects are locally scoped, and noone else has a reference,
   103                     // so we release them to clean up ressources.
   104                     Marshal.FinalReleaseComObject(asMail);
   105                 }
   106             }
   107             catch (Exception ex)
   108             {
   109                 Debug.Print(ex.ToString());
   110 
   111                 Debug.Print("Using unconverted attachment data.");
   112                 return File.ReadAllBytes(tempFile);
   113             }
   114             finally
   115             {
   116                 // Delete temp file
   117                 try
   118                 {
   119                     File.Delete(tempFile);
   120                 }
   121                 catch (Exception ex)
   122                 {
   123                     Debug.Print(ex.ToString());
   124                 }
   125             }
   126         }
   127 
   128         /// <summary>
   129         /// Flags used during conversion of Mails using <see cref="IConverterSession.MAPIToMIMEStm"/>.
   130         /// </summary>
   131         /// <remarks>The documentation mentions CCSF_EMBEDDED_MESSAGE to persist the Sent/Unsent
   132         /// information in a X-Unsent header, but does not document its bit value...</remarks>
   133         [Flags]
   134         public enum CCSF// : int
   135         {
   136             /// <summary>
   137             /// Must be always set according to MS documentation.
   138             /// </summary>
   139             SMTP = 0x0002,
   140 
   141             /// <summary>
   142             /// Ignore the headers of the outside message. We probably don't want to do that.
   143             /// </summary>
   144             NOHEADERS = 0x0004,
   145 
   146             /// <summary>
   147             /// Use the Transport Neutral Encapsulation Format (TNEF). We propbably don't want to do that.
   148             /// </summary>
   149             USE_TNEF = 0x0010,
   150 
   151             /// <summary>
   152             /// Whether to include BCC headers. TODO: Unsure how to set this option, we currently default to
   153             /// exclude it, to protect the privacy (and most probably, BCC headers were not present in any
   154             /// forwarded mail in the beginning.)
   155             /// </summary>
   156             INCLUDE_BCC = 0x0020,
   157 
   158             /// <summary>
   159             /// Whether the converter should allow 8 bit headers. We should set this, to not drop headers
   160             /// which were part of the original message.
   161             /// </summary>
   162             /// <remarks>This is CCSF_8BITHEADERS in the original.</remarks>
   163             EightBITHEADERS = 0x0040,
   164 
   165             /// <summary>
   166             /// This flag is not for us - we probably do not want to autoconvert HTML to RTF during the export.
   167             /// </summary>
   168             USE_RTF = 0x0080,
   169 
   170             /// <summary>
   171             /// Whether only plan text should be sent - we should probabyl not use this, we want to retain
   172             /// everything which was in the original message.
   173             /// </summary>
   174             PLAIN_TEXT_ONLY = 0x1000,
   175 
   176             /// <summary>
   177             /// Whether the Message ID should be omitted, we currently default to false, to be lossless.
   178             /// </summary>
   179             NO_MSGID = 0x4000,
   180 
   181         }
   182 
   183         /// <summary>
   184         /// Minimal wrapper of the IStream interface around a Memory Stream,
   185         /// it just implements the methods necessary for <see cref="IConverterSession.MAPIToMIMEStm"/>.
   186         /// </summary>
   187         /// <remarks>
   188         /// There seems to be no standard conversion mechanism between <see cref="IStream"/>
   189         /// and <see cref="Stream"/> - somehow unbelievable... :-)</remarks>
   190         internal class MemoryStreamComMinimalWrapper : MemoryStream, IStream
   191         {
   192 
   193             public void Clone(out IStream ppstm)
   194             {
   195                 throw new NotImplementedException();
   196             }
   197 
   198             public void Commit(int grfCommitFlags)
   199             {
   200             }
   201 
   202             public void CopyTo(IStream pstm, long cb, IntPtr pcbRead, IntPtr pcbWritten)
   203             {
   204                 throw new NotImplementedException();
   205             }
   206 
   207             public void LockRegion(long libOffset, long cb, int dwLockType)
   208             {
   209                 throw new NotImplementedException();
   210             }
   211 
   212             public void Read(byte[] pv, int cb, IntPtr pcbRead)
   213             {
   214                 throw new NotImplementedException();
   215             }
   216 
   217             public void Revert()
   218             {
   219                 throw new NotImplementedException();
   220             }
   221 
   222             public void Seek(long dlibMove, int dwOrigin, IntPtr plibNewPosition)
   223             {
   224                 var newpos = Seek(dlibMove, (SeekOrigin)dwOrigin);
   225                 if (plibNewPosition != IntPtr.Zero)
   226                     Marshal.WriteInt64(plibNewPosition, newpos);
   227             }
   228 
   229             public void SetSize(long libNewSize)
   230             {
   231                 base.Capacity = (int)libNewSize;
   232             }
   233 
   234             public void Stat(out System.Runtime.InteropServices.ComTypes.STATSTG pstatstg, int grfStatFlag)
   235             {
   236                 throw new NotImplementedException();
   237             }
   238 
   239             public void UnlockRegion(long libOffset, long cb, int dwLockType)
   240             {
   241                 throw new NotImplementedException();
   242             }
   243 
   244             public void Write(byte[] pv, int cb, IntPtr pcbWritten)
   245             {
   246                 base.Write(pv, 0, cb);
   247                 if (pcbWritten != IntPtr.Zero)
   248                     Marshal.WriteInt32(pcbWritten, cb);
   249             }
   250         }
   251 
   252         /// <summary>
   253         /// The encoding type.
   254         /// </summary>
   255         public enum EncodingType
   256         {
   257             IET_BINARY = 0, // currently not supported by IConverterSession.
   258 
   259             /// <summary>
   260             /// Base 64 encoding
   261             /// </summary>
   262             IET_BASE64 = 1,
   263             IET_UUENCODE = 2,
   264             IET_QP = 3,
   265             IET_7BIT = 4,
   266             IET_8BIT = 5,
   267 
   268             IET_INETCSET = 6,// currently not supported by IConverterSession.
   269             IET_UNICODE = 7,// currently not supported by IConverterSession.
   270             IET_RFC1522 = 8,// currently not supported by IConverterSession.
   271             IET_ENCODED = 9,// currently not supported by IConverterSession.
   272             IET_CURRENT = 10,// currently not supported by IConverterSession.
   273             IET_UNKNOWN = 11,// currently not supported by IConverterSession.
   274             IET_BINHEX40 = 12,// currently not supported by IConverterSession.
   275             IET_LAST = 13// currently not supported by IConverterSession.
   276         }
   277 
   278         /// <summary>
   279         /// Whether to use MIME (RFC 1521) or the plain old RFC 822.
   280         /// </summary>
   281         public enum MimeSaveType
   282         {
   283             SAVE_RFC822 = 0,
   284             SAVE_RFC1521 = 1
   285         }
   286 
   287         [ComVisible(false)]
   288         [ComImport()]
   289         [Guid("00020307-0000-0000-C000-000000000046")]
   290         public interface IMessage
   291         {
   292 
   293         }
   294 
   295         /// <summary>
   296         /// Minimal hackish declaration of the IConverterSession interface, which allows us to call
   297         /// the methods via COM. We need to keep the VTable order, thus the declaration of methods
   298         /// we do not use.
   299         /// </summary>
   300         /// <seealso cref="https://msdn.microsoft.com/de-de/library/office/ff960417.aspx"/>
   301         /// <seealso cref="https://msdn.microsoft.com/DE-DE/library/office/ff960231.aspx"/>
   302         [ComImport]
   303         [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
   304         [Guid("4b401570-b77b-11d0-9da5-00c04fd65685")]
   305         public interface IConverterSession
   306         {
   307             // This method is documented as HRESULT SetAdrBook(LPADRBOOK pab) in some sources.
   308             // Specifies an optional MAPI address book which is used to resolve ambigous addresses.
   309             // In our case, we don't have easy access to an MAPI address book, and additionally, we
   310             // can assume that all addresses have already been resolved as it is a forwarded mail.
   311             [PreserveSig]
   312             int Placeholder0();
   313 
   314             /// <summary>
   315             /// Sets the encoding of the outgoing mail.
   316             /// </summary>
   317             /// <param name="DispId"></param>
   318             /// <returns></returns>
   319             [PreserveSig]
   320             int SetEncoding([In, MarshalAs(UnmanagedType.I4)] EncodingType DispId);
   321 
   322             // Currently neither supported nor documented by Microsoft.
   323             [PreserveSig]
   324             int Placeholder1();
   325 
   326             // Converts a MIME stream to a MAPI mail object - it is unclear whether
   327             // we will be able to make use of this method one day...
   328             [PreserveSig]
   329             int MIMEToMAPI(
   330                 [In, MarshalAs(UnmanagedType.Interface)] IStream pstm, // LPStream
   331                 [Out, MarshalAs(UnmanagedType.Interface)] IntPtr pmsg, // LPMessage
   332                 IntPtr pszSrcSrv, // must be null
   333                 [MarshalAs(UnmanagedType.I4)] CCSF ulFlags // Only some specific CCSF flags are allowed here.
   334             );
   335 
   336             /// <summary>
   337             /// Conversion of a MAPI object to a stream containing a MIME / EML Mail.
   338             /// </summary>
   339             /// <param name="pmsg">The Message (IntPtr ot the LPMessage).</param>
   340             /// <param name="pstm">The <see cref="IStream"/> implementation where the message will be written to.</param>
   341             /// <param name="ulFlags">The flags - the flag <see cref="CCSF.SMTP"/> MUST be set according to MS documentation.</param>
   342             /// <returns></returns>
   343             [PreserveSig]
   344             int MAPIToMIMEStm(
   345                 [In, MarshalAs(UnmanagedType.Interface)] IMessage pmsg, // LPMessage
   346                 [Out, MarshalAs(UnmanagedType.Interface)] IStream pstm, // LPStream
   347                 [MarshalAs(UnmanagedType.I4)] CCSF ulFlags = CCSF.SMTP
   348             );
   349 
   350             // Currently neither supported nor documented by Microsoft.
   351             [PreserveSig]
   352             int Placeholder2();
   353 
   354             // Currently neither supported nor documented by Microsoft.
   355             [PreserveSig]
   356             int Placeholder3();
   357 
   358             // Currently neither supported nor documented by Microsoft.
   359             [PreserveSig]
   360             int Placeholder4();
   361 
   362             /// <summary>
   363             /// Configure whether text should be wrapped or not, and if yes, at which width.
   364             /// </summary>
   365             /// <param name="fWrapText">true if the text should be wrapped.</param>
   366             /// <param name="ulWrapWidth">The text wrapping width.</param>
   367             /// <returns>HRESULT</returns>
   368             [PreserveSig]
   369             int SetTextWrapping(
   370             bool fWrapText,
   371             uint ulWrapWidth
   372             );
   373 
   374             /// <summary>
   375             /// Whether to save the message according to RFC 822 or using 
   376             /// MIME (RFC 1521).
   377             /// </summary>
   378             /// <param name="mstSaveFormat"></param>
   379             /// <returns></returns>
   380             [PreserveSig]
   381             int SetSaveFormat([In, MarshalAs(UnmanagedType.I4)]MimeSaveType mstSaveFormat);
   382 
   383             // Currently neither supported nor documented by Microsoft.
   384             [PreserveSig]
   385             int Placeholder5();
   386 
   387             // This method is documentet as SetCharSet in some documentations.
   388             // It seems that we can live with the default charset, and additionally,
   389             // the MimeOleGetCodePageCharSet() function needed to get the necessary
   390             // handle for a charset is documented as "do not use."...
   391             [PreserveSig]
   392             int Placeholder6();
   393         }
   394     }
   395 }
   396