MsgConverter.cs
author Markus Schaber <markus@pep-security.net>
Sat, 28 May 2016 10:49:40 +0200
branchunregister_callbacks
changeset 940 f349ba4cdcba
parent 910 9bd60ca2c6ac
child 1444 2458d5d2d562
permissions -rw-r--r--
Closed the wrongly named branch.
markus@200
     1

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