2013-11-14

Congratulations, your tool has users! But now complaints are pouring in. Unfortunately, very few developers will have the time or patience to send poke around or follow instructions for finding your debugging output to help you figure things out.

With a single click, the following code opens up a user's default email client (e.g. Outlook) addressed to you, with the file attached and a body explaining the data being sent and an option to see it. Then, they just have to click send. There are no third party dependencies.

Zipping a file

automark.VisualStudio/automark.VisualStudioPackage.cs

First, setup things on disk. builder is a StringBuilder that contains the exported contents. tempExport is the file we want to zip, as tempExportZip.

var time = DateTime.Now;
string tempExport = System.IO.Path.Combine(m_basePath, "exports", string.Format("export-{0:yyyy-MM-dd-hh-mm-tt}.export", time));
string tempExportZip = System.IO.Path.Combine(m_basePath, "exports", string.Format("export-{0:yyyy-MM-dd-hh-mm-tt}.zip", time));

var parent = System.IO.Path.GetDirectoryName(tempExport);
if (!System.IO.Directory.Exists(parent))
{
    System.IO.Directory.CreateDirectory(parent);
}

System.IO.File.WriteAllText(tempExport, builder.ToString());
Zip.ZipFile(tempExportZip, tempExport);

automark.VisualStudio/Util/Zip.cs

This is a helper class using ZipPackage in .NET 4.0 to zip files. There are better options (ZipFile) if you only have to support .NET 4.5. No third party dependencies!

 using System.IO;
 using System.IO.Packaging;

 class Zip
 {
     public static void ZipFile(string zipFilename, string filenameToAdd)
     {
         using (Package zipPackage = ZipPackage.Open(zipFilename, FileMode.OpenOrCreate))
         {
             string destFilename = ".\\" + Path.GetFileName(filenameToAdd);
             Uri zipPartUri = PackUriHelper.CreatePartUri(new Uri(destFilename, UriKind.Relative));

             if (zipPackage.PartExists(zipPartUri))
             {
                 zipPackage.DeletePart(zipPartUri);
             }

             PackagePart zipPackagePart = zipPackage.CreatePart(zipPartUri, "", CompressionOption.Maximum);
             using (FileStream fileStream = new FileStream(filenameToAdd, FileMode.Open, FileAccess.Read))
             {
                 using (Stream dest = zipPackagePart.GetStream())
                 {
                     fileStream.CopyTo(dest);
                 }
             }
         }
     }
 }

Sending mail

Using the MAPI api, we can automatically attach an file and open a new email message in Window's default mail client. The MapiMailMessage helper class is shown at the end of the post.

automark.VisualStudio/automark.VisualStudioPackage.cs

Let's send the message! As a back-up, we can use the mailto: protocol if the MAPI api fails. But we have to ask the user to manually find and attach the zip file.

bool triedBackup = false;
try
{
    MapiMailMessage message = new MapiMailMessage("Automark export", "This information includes the automark usage log....  This usage info will help in testing and improving the tool. You can review the exported info in " + tempExport + "  \nThanks!");
    message.Recipients.Add("me@gmail.com");
    message.Files.Add(tempExportZip);
    message.OnDone += (success) =>
    {
        if (!success)
        {
            triedBackup = true;
            string msg = @"mailto:me@gmail.com&subject=Automark export&body=Please attach {0} and send.";
            System.Diagnostics.Process.Start(string.Format(msg, tempExportZip));
        }
    };
    message.ShowDialog();
}
catch (Exception ex)
{
    if (!triedBackup)
    {
        string msg = @"mailto:me@gmail.com&subject=Automark export&body=Please attach {0} and send.";
        System.Diagnostics.Process.Start(string.Format(msg, tempExportZip));
    }
}

Conclusion

Reducing friction to feedback helps us get better data. There are some nice ways to extend this. For example, automatically take a screenshot of buggy behavior and send it over email. Or file a bug report to github, etc.

Appendix - MAPI Helper class

Helper class for using PInvoke to access MAPI windows api. Also see this Stack Overflow Question.

automark.VisualStudio/Util/Mail.cs

using System;
using System.Collections;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;

namespace ninlabs.automark.VisualStudio.Util
{
    public class TestMapiMessageClass
    {
        /// <summary>
        /// Test method to create and show an email
        /// </summary>
        /// <param name="args"></param>
        static void Main(string[] args)
        {
            MapiMailMessage message = new MapiMailMessage("Test Message", "Test Body");
            message.Recipients.Add("Test@Test.com");
            message.Files.Add(@"C:\del.txt");
            message.ShowDialog();
            Console.ReadLine();
        }
    }

    public delegate void MessageEvent(bool success);

    public class MapiMailMessage
    {

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
        private class MapiFileDescriptor
        {
            public int reserved = 0;
            public int flags = 0;
            public int position = 0;
            public string path = null;
            public string name = null;
            public IntPtr type = IntPtr.Zero;
        }

        public enum RecipientType : int
        {
            To = 1,
            CC = 2,
            BCC = 3
        };

        public event MessageEvent OnDone;

        private string _subject;
        private string _body;
        private RecipientCollection _recipientCollection;
        private ArrayList _files;
        private ManualResetEvent _manualResetEvent;

        public MapiMailMessage()
        {
            _files = new ArrayList();
            _recipientCollection = new RecipientCollection();
            _manualResetEvent = new ManualResetEvent(false);
        }

        public MapiMailMessage(string subject)
            : this()
        {
            _subject = subject;
        }

        public MapiMailMessage(string subject, string body)
            : this()
        {
            _subject = subject;
            _body = body;
        }

        public string Subject
        {
            get { return _subject; }
            set { _subject = value; }
        }

        public string Body
        {
            get { return _body; }
            set { _body = value; }
        }

        public RecipientCollection Recipients
        {
            get { return _recipientCollection; }
        }

        public ArrayList Files
        {
            get { return _files; }
        }

        public void ShowDialog()
        {
            // Create the mail message in an STA thread
            Thread t = new Thread(new ThreadStart(_ShowMail));
            t.IsBackground = true;
            t.ApartmentState = ApartmentState.STA;
            t.Start();

            // only return when the new thread has built it's interop representation
            _manualResetEvent.WaitOne();
            _manualResetEvent.Reset();
        }

        private void _ShowMail(object ignore)
        {
            MAPIHelperInterop.MapiMessage message = new MAPIHelperInterop.MapiMessage();

            using (RecipientCollection.InteropRecipientCollection interopRecipients
                        = _recipientCollection.GetInteropRepresentation())
            {

                message.Subject = _subject;
                message.NoteText = _body;

                message.Recipients = interopRecipients.Handle;
                message.RecipientCount = _recipientCollection.Count;

                // Check if we need to add attachments
                if (_files.Count > 0)
                {
                    // Add attachments
                    message.Files = _AllocAttachments(out message.FileCount);
                }

                // Signal the creating thread (make the remaining code async)
                _manualResetEvent.Set();

                const int MAPI_DIALOG = 0x8;
                //const int MAPI_LOGON_UI = 0x1;
                const int SUCCESS_SUCCESS = 0;
                int error = MAPIHelperInterop.MAPISendMail(IntPtr.Zero, IntPtr.Zero, message, MAPI_DIALOG, 0);

                if (_files.Count > 0)
                {
                    // Deallocate the files
                    _DeallocFiles(message);
                }

                // Check for error
                if (error != SUCCESS_SUCCESS)
                {
                    _LogErrorMapi(error);
                }

                if (OnDone != null)
                {
                    OnDone(error == SUCCESS_SUCCESS || error == 1 /*MAPI_USER_ABORT*/);
                }
            }
        }

        private void _DeallocFiles(MAPIHelperInterop.MapiMessage message)
        {
            if (message.Files != IntPtr.Zero)
            {
                Type fileDescType = typeof(MapiFileDescriptor);
                int fsize = Marshal.SizeOf(fileDescType);

                // Get the ptr to the files
                int runptr = (int)message.Files;
                // Release each file
                for (int i = 0; i < message.FileCount; i++)
                {
                    Marshal.DestroyStructure((IntPtr)runptr, fileDescType);
                    runptr += fsize;
                }
                // Release the file
                Marshal.FreeHGlobal(message.Files);
            }
        }

        private IntPtr _AllocAttachments(out int fileCount)
        {
            fileCount = 0;
            if (_files == null)
            {
                return IntPtr.Zero;
            }
            if ((_files.Count <= 0) || (_files.Count > 100))
            {
                return IntPtr.Zero;
            }

            Type atype = typeof(MapiFileDescriptor);
            int asize = Marshal.SizeOf(atype);
            IntPtr ptra = Marshal.AllocHGlobal(_files.Count * asize);

            MapiFileDescriptor mfd = new MapiFileDescriptor();
            mfd.position = -1;
            int runptr = (int)ptra;
            for (int i = 0; i < _files.Count; i++)
            {
                string path = _files[i] as string;
                mfd.name = Path.GetFileName(path);
                mfd.path = path;
                Marshal.StructureToPtr(mfd, (IntPtr)runptr, false);
                runptr += asize;
            }

            fileCount = _files.Count;
            return ptra;
        }

        /// <summary>
        /// Sends the mail message.
        /// </summary>
        private void _ShowMail()
        {
            _ShowMail(null);
        }

        /// <summary>
        /// Logs any Mapi errors.
        /// </summary>
        private void _LogErrorMapi(int errorCode)
        {
            const int MAPI_USER_ABORT = 1;
            const int MAPI_E_FAILURE = 2;
            const int MAPI_E_LOGIN_FAILURE = 3;
            const int MAPI_E_DISK_FULL = 4;
            const int MAPI_E_INSUFFICIENT_MEMORY = 5;
            const int MAPI_E_BLK_TOO_SMALL = 6;
            const int MAPI_E_TOO_MANY_SESSIONS = 8;
            const int MAPI_E_TOO_MANY_FILES = 9;
            const int MAPI_E_TOO_MANY_RECIPIENTS = 10;
            const int MAPI_E_ATTACHMENT_NOT_FOUND = 11;
            const int MAPI_E_ATTACHMENT_OPEN_FAILURE = 12;
            const int MAPI_E_ATTACHMENT_WRITE_FAILURE = 13;
            const int MAPI_E_UNKNOWN_RECIPIENT = 14;
            const int MAPI_E_BAD_RECIPTYPE = 15;
            const int MAPI_E_NO_MESSAGES = 16;
            const int MAPI_E_INVALID_MESSAGE = 17;
            const int MAPI_E_TEXT_TOO_LARGE = 18;
            const int MAPI_E_INVALID_SESSION = 19;
            const int MAPI_E_TYPE_NOT_SUPPORTED = 20;
            const int MAPI_E_AMBIGUOUS_RECIPIENT = 21;
            const int MAPI_E_MESSAGE_IN_USE = 22;
            const int MAPI_E_NETWORK_FAILURE = 23;
            const int MAPI_E_INVALID_EDITFIELDS = 24;
            const int MAPI_E_INVALID_RECIPS = 25;
            const int MAPI_E_NOT_SUPPORTED = 26;
            const int MAPI_E_NO_LIBRARY = 999;
            const int MAPI_E_INVALID_PARAMETER = 998;

            string error = string.Empty;
            switch (errorCode)
            {
                case MAPI_USER_ABORT:
                    error = "User Aborted.";
                    break;
                case MAPI_E_FAILURE:
                    error = "MAPI Failure.";
                    break;
                case MAPI_E_LOGIN_FAILURE:
                    error = "Login Failure.";
                    break;
                case MAPI_E_DISK_FULL:
                    error = "MAPI Disk full.";
                    break;
                case MAPI_E_INSUFFICIENT_MEMORY:
                    error = "MAPI Insufficient memory.";
                    break;
                case MAPI_E_BLK_TOO_SMALL:
                    error = "MAPI Block too small.";
                    break;
                case MAPI_E_TOO_MANY_SESSIONS:
                    error = "MAPI Too many sessions.";
                    break;
                case MAPI_E_TOO_MANY_FILES:
                    error = "MAPI too many files.";
                    break;
                case MAPI_E_TOO_MANY_RECIPIENTS:
                    error = "MAPI too many recipients.";
                    break;
                case MAPI_E_ATTACHMENT_NOT_FOUND:
                    error = "MAPI Attachment not found.";
                    break;
                case MAPI_E_ATTACHMENT_OPEN_FAILURE:
                    error = "MAPI Attachment open failure.";
                    break;
                case MAPI_E_ATTACHMENT_WRITE_FAILURE:
                    error = "MAPI Attachment Write Failure.";
                    break;
                case MAPI_E_UNKNOWN_RECIPIENT:
                    error = "MAPI Unknown recipient.";
                    break;
                case MAPI_E_BAD_RECIPTYPE:
                    error = "MAPI Bad recipient type.";
                    break;
                case MAPI_E_NO_MESSAGES:
                    error = "MAPI No messages.";
                    break;
                case MAPI_E_INVALID_MESSAGE:
                    error = "MAPI Invalid message.";
                    break;
                case MAPI_E_TEXT_TOO_LARGE:
                    error = "MAPI Text too large.";
                    break;
                case MAPI_E_INVALID_SESSION:
                    error = "MAPI Invalid session.";
                    break;
                case MAPI_E_TYPE_NOT_SUPPORTED:
                    error = "MAPI Type not supported.";
                    break;
                case MAPI_E_AMBIGUOUS_RECIPIENT:
                    error = "MAPI Ambiguous recipient.";
                    break;
                case MAPI_E_MESSAGE_IN_USE:
                    error = "MAPI Message in use.";
                    break;
                case MAPI_E_NETWORK_FAILURE:
                    error = "MAPI Network failure.";
                    break;
                case MAPI_E_INVALID_EDITFIELDS:
                    error = "MAPI Invalid edit fields.";
                    break;
                case MAPI_E_INVALID_RECIPS:
                    error = "MAPI Invalid Recipients.";
                    break;
                case MAPI_E_NOT_SUPPORTED:
                    error = "MAPI Not supported.";
                    break;
                case MAPI_E_NO_LIBRARY:
                    error = "MAPI No Library.";
                    break;
                case MAPI_E_INVALID_PARAMETER:
                    error = "MAPI Invalid parameter.";
                    break;
            }

            Debug.WriteLine("Error sending MAPI Email. Error: " + error + " (code = " + errorCode + ").");
        }

        internal class MAPIHelperInterop
        {
            private MAPIHelperInterop()
            {
            }

            public const int MAPI_LOGON_UI = 0x1;

            [DllImport("MAPI32.DLL", CharSet = CharSet.Ansi)]
            public static extern int MAPILogon(IntPtr hwnd, string prf, string pw, int flg, int rsv, ref IntPtr sess);

            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
            public class MapiMessage
            {
                public int Reserved = 0;
                public string Subject = null;
                public string NoteText = null;
                public string MessageType = null;
                public string DateReceived = null;
                public string ConversationID = null;
                public int Flags = 0;
                public IntPtr Originator = IntPtr.Zero;
                public int RecipientCount = 0;
                public IntPtr Recipients = IntPtr.Zero;
                public int FileCount = 0;
                public IntPtr Files = IntPtr.Zero;
            }

            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
            public class MapiRecipDesc
            {
                public int Reserved = 0;
                public int RecipientClass = 0;
                public string Name = null;
                public string Address = null;
                public int eIDSize = 0;
                public IntPtr EntryID = IntPtr.Zero;
            }

            [DllImport("MAPI32.DLL")]
            public static extern int MAPISendMail(IntPtr session, IntPtr hwnd, MapiMessage message, int flg, int rsv);
        }
    }

    public class Recipient
    {
        public string Address = null;
        public string DisplayName = null;
        public MapiMailMessage.RecipientType RecipientType = MapiMailMessage.RecipientType.To;

        public Recipient(string address)
        {
            Address = address;
        }

        public Recipient(string address, string displayName)
        {
            Address = address;
            DisplayName = displayName;
        }

        public Recipient(string address, MapiMailMessage.RecipientType recipientType)
        {
            Address = address;
            RecipientType = recipientType;
        }

        public Recipient(string address, string displayName, MapiMailMessage.RecipientType recipientType)
        {
            Address = address;
            DisplayName = displayName;
            RecipientType = recipientType;
        }

        internal MapiMailMessage.MAPIHelperInterop.MapiRecipDesc GetInteropRepresentation()
        {
            MapiMailMessage.MAPIHelperInterop.MapiRecipDesc interop = new MapiMailMessage.MAPIHelperInterop.MapiRecipDesc();

            if (DisplayName == null)
            {
                interop.Name = Address;
            }
            else
            {
                interop.Name = DisplayName;
                interop.Address = Address;
            }

            interop.RecipientClass = (int)RecipientType;

            return interop;
        }
    }
    public class RecipientCollection : CollectionBase
    {
        /// <summary>
        /// Adds the specified recipient to this collection.
        /// </summary>
        public void Add(Recipient value)
        {
            List.Add(value);
        }

        /// <summary>
        /// Adds a new recipient with the specified address to this collection.
        /// </summary>
        public void Add(string address)
        {
            this.Add(new Recipient(address));
        }

        /// <summary>
        /// Adds a new recipient with the specified address and display name to this collection.
        /// </summary>
        public void Add(string address, string displayName)
        {
            this.Add(new Recipient(address, displayName));
        }

        /// <summary>
        /// Adds a new recipient with the specified address and recipient type to this collection.
        /// </summary>
        public void Add(string address, MapiMailMessage.RecipientType recipientType)
        {
            this.Add(new Recipient(address, recipientType));
        }

        /// <summary>
        /// Adds a new recipient with the specified address, display name and recipient type to this collection.
        /// </summary>
        public void Add(string address, string displayName, MapiMailMessage.RecipientType recipientType)
        {
            this.Add(new Recipient(address, displayName, recipientType));
        }

        /// <summary>
        /// Returns the recipient stored in this collection at the specified index.
        /// </summary>
        public Recipient this[int index]
        {
            get
            {
                return (Recipient)List[index];
            }
        }

        internal InteropRecipientCollection GetInteropRepresentation()
        {
            return new InteropRecipientCollection(this);
        }

        internal struct InteropRecipientCollection : IDisposable
        {
            private IntPtr _handle;
            private int _count;

            public InteropRecipientCollection(RecipientCollection outer)
            {
                _count = outer.Count;

                if (_count == 0)
                {
                    _handle = IntPtr.Zero;
                    return;
                }

                // allocate enough memory to hold all recipients
                int size = Marshal.SizeOf(typeof(MapiMailMessage.MAPIHelperInterop.MapiRecipDesc));
                _handle = Marshal.AllocHGlobal(_count * size);

                // place all interop recipients into the memory just allocated
                int ptr = (int)_handle;
                foreach (Recipient native in outer)
                {
                    MapiMailMessage.MAPIHelperInterop.MapiRecipDesc interop = native.GetInteropRepresentation();

                    // stick it in the memory block
                    Marshal.StructureToPtr(interop, (IntPtr)ptr, false);
                    ptr += size;
                }
            }

            public IntPtr Handle
            {
                get { return _handle; }
            }
            public void Dispose()
            {
                if (_handle != IntPtr.Zero)
                {
                    Type type = typeof(MapiMailMessage.MAPIHelperInterop.MapiRecipDesc);
                    int size = Marshal.SizeOf(type);

                    // destroy all the structures in the memory area
                    int ptr = (int)_handle;
                    for (int i = 0; i < _count; i++)
                    {
                        Marshal.DestroyStructure((IntPtr)ptr, type);
                        ptr += size;
                    }

                    // free the memory
                    Marshal.FreeHGlobal(_handle);

                    _handle = IntPtr.Zero;
                    _count = 0;
                }
            }
        }
    }
}