Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VB

Programmatically adding attachments to emails in C# and VB.NET

4.82/5 (95 votes)
20 Apr 2007CPOL5 min read 16   10K  
A technique for programmatically adding attachments to emails in C# and VB.NET.

Screenshot - SendToNET2.jpg

Updates

By popular demand, this article has been updated to provide for multiple attachments and recipients.

Introduction

Not unlike The Terminator, my previous article explaining how to programmatically add attachments to emails was meant to be a low budget offering for a very straightforward but useful addition to your GUIs. I hadn't quite expected the tsunami of emails and comments, particularly questions about how to do all this in C# and VB.NET. Hopefully, this article will answer those questions and be like Terminator 2 - the ultimate sequel.

And like all good sequels, there are problems along the way. It's not actors wanting exorbitant fees that's the issue. For us, it's the pain of calling unmanaged code - MAPI32 - from managed code. We'll need to delve into the world of .NET Interop Marshaling to explain how some of this works. We'll be using PInvoke - a.k.a. Platform Invoke - to make this all work.

The sample code provides both C# and VB.NET versions, with a simple test program for each.

There's a new Marshaler in town

.NET provides a language-independent development system enabling you to write classes in VB, C++, or C#, and use them in other languages; you can even derive from classes in a different language. But how do you call a function in an unmanaged DLL? You have to somehow convert your C# and VB objects into the necessary structs, char*'s, and function pointers the unmanaged DLL expects. In other words, your parameters must be marshaled.

Marshaling is a complex topic (see MSDN), and Microsoft has gone to great lengths to support unmanaged code interop and COM interop, so I guess they think unmanaged code will be around for a while! Fortunately, the parts we need for this example are pretty straightforward. To call a DLL function from C#, you must declare a function that will tell the compiler where the exported entry point is in the unmanaged DLL - in this case MAPI32.DLL. The DLLImport attribute does this for you.

C#
using System.Runtime.InteropServices;

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

The interop in .NET provides default marshaling to handle common types for parameters like strings and ints. Unfortunately, many Win32 API calls use pointers to unmanaged structs so you have to manage your own marshaling for these. You need to create a C# class or struct that mirrors the required unmanaged struct, choose the necessary types within the struct, and mark it up with the [StructLayout] attribute. This is necessary because the CLR controls the physical layout of memory, so if the class needs to be arranged in an expected way, use [StructLayout] to preserve the layout, and .NET will not mess with it.

Default marshaling gotchas

  • If the unmanaged call uses a LPTSTR out parameter, map this to a StringBuilder rather than String. Strings aren't mutable, and the default marshaling type for StringBuilder is also an LPTSTR.
  • Use ref to mark up out parameters or some equivalent. I've used IntPtr in this sample. It's an interop supplied, platform specific type that can be used to represent a pointer or handle.
  • If you make the call, you may get this friendly message at runtime:

    Screenshot - SendToNET1.JPG

    • Check for a mismatch between your unmanaged function signature and the declared [DllImport] C# function declaration.
    • Check the parameter types - particularly Strings vs. StringBuilder and the use of ref.
    • Check that the size of struct parameters match that required by the unmanaged function.
    • Check the types and size of your struct members.

The C# way

C#
using System;
using System.Runtime.InteropServices;
using System.IO;
using System.Collections.Generic;
using System.Windows.Forms;

namespace SendFileTo
{
    class MAPI
    {
        public bool AddRecipientTo(string email)
        {
            return AddRecipient(email, HowTo.MAPI_TO);
        }

        public bool AddRecipientCC(string email)
        {
            return AddRecipient(email, HowTo.MAPI_TO);
        }

        public bool AddRecipientBCC(string email)
        {
            return AddRecipient(email, HowTo.MAPI_TO);
        }

        public void AddAttachment(string strAttachmentFileName)
        {
            m_attachments.Add(strAttachmentFileName);
        }

        public int SendMailPopup(string strSubject, string strBody)
        {
            return SendMail(strSubject, strBody, MAPI_LOGON_UI | MAPI_DIALOG);
        }

        public int SendMailDirect(string strSubject, string strBody)
        {
            return SendMail(strSubject, strBody, MAPI_LOGON_UI);
        }


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

        int SendMail(string strSubject, string strBody, int how)
        {
            MapiMessage msg = new MapiMessage();
            msg.subject = strSubject;
            msg.noteText = strBody;

            msg.recips = GetRecipients(out msg.recipCount);
            msg.files = GetAttachments(out msg.fileCount);

            m_lastError = MAPISendMail(new IntPtr(0), new IntPtr(0), msg, how, 
                0);
            if (m_lastError > 1)
                MessageBox.Show("MAPISendMail failed! " + GetLastError(), 
                    "MAPISendMail");

            Cleanup(ref msg);
            return m_lastError;
        }

        bool AddRecipient(string email, HowTo howTo)
        {
            MapiRecipDesc recipient = new MapiRecipDesc();

            recipient.recipClass = (int)howTo;
            recipient.name = email;
            m_recipients.Add(recipient);

            return true;
        }

        IntPtr GetRecipients(out int recipCount)
        {
            recipCount = 0;
            if (m_recipients.Count == 0)
                return IntPtr.Zero;

            int size = Marshal.SizeOf(typeof(MapiRecipDesc));
            IntPtr intPtr = Marshal.AllocHGlobal(m_recipients.Count * size);

            int ptr = (int)intPtr;
            foreach (MapiRecipDesc mapiDesc in m_recipients)
            {
                Marshal.StructureToPtr(mapiDesc, (IntPtr)ptr, false);
                ptr += size;
            }

            recipCount = m_recipients.Count;
            return intPtr;
        }

        IntPtr GetAttachments(out int fileCount)
        {
            fileCount = 0;
            if (m_attachments == null)
                return IntPtr.Zero;

            if ((m_attachments.Count <= 0) || (m_attachments.Count > 
                maxAttachments))
                return IntPtr.Zero;

            int size = Marshal.SizeOf(typeof(MapiFileDesc));
            IntPtr intPtr = Marshal.AllocHGlobal(m_attachments.Count * size);

            MapiFileDesc mapiFileDesc = new MapiFileDesc();
            mapiFileDesc.position = -1;
            int ptr = (int)intPtr;
            
            foreach (string strAttachment in m_attachments)
            {
                mapiFileDesc.name = Path.GetFileName(strAttachment);
                mapiFileDesc.path = strAttachment;
                Marshal.StructureToPtr(mapiFileDesc, (IntPtr)ptr, false);
                ptr += size;
            }

            fileCount = m_attachments.Count;
            return intPtr;
        }

        void Cleanup(ref MapiMessage msg)
        {
            int size = Marshal.SizeOf(typeof(MapiRecipDesc));
            int ptr = 0;

            if (msg.recips != IntPtr.Zero)
            {
                ptr = (int)msg.recips;
                for (int i = 0; i < msg.recipCount; i++)
                {
                    Marshal.DestroyStructure((IntPtr)ptr, 
                        typeof(MapiRecipDesc));
                    ptr += size;
                }
                Marshal.FreeHGlobal(msg.recips);
            }

            if (msg.files != IntPtr.Zero)
            {
                size = Marshal.SizeOf(typeof(MapiFileDesc));

                ptr = (int)msg.files;
                for (int i = 0; i < msg.fileCount; i++)
                {
                    Marshal.DestroyStructure((IntPtr)ptr, 
                        typeof(MapiFileDesc));
                    ptr += size;
                }
                Marshal.FreeHGlobal(msg.files);
            }
            
            m_recipients.Clear();
            m_attachments.Clear();
            m_lastError = 0;
        }
        
        public string GetLastError()
        {
            if (m_lastError <= 26)
                return errors[ m_lastError ];
            return "MAPI error [" + m_lastError.ToString() + "]";
        }

        readonly string[] errors = new string[] {
        "OK [0]", "User abort [1]", "General MAPI failure [2]", 
                "MAPI login failure [3]", "Disk full [4]", 
                "Insufficient memory [5]", "Access denied [6]", 
                "-unknown- [7]", "Too many sessions [8]", 
                "Too many files were specified [9]", 
                "Too many recipients were specified [10]", 
                "A specified attachment was not found [11]",
        "Attachment open failure [12]", 
                "Attachment write failure [13]", "Unknown recipient [14]", 
                "Bad recipient type [15]", "No messages [16]", 
                "Invalid message [17]", "Text too large [18]", 
                "Invalid session [19]", "Type not supported [20]", 
                "A recipient was specified ambiguously [21]", 
                "Message in use [22]", "Network failure [23]",
        "Invalid edit fields [24]", "Invalid recipients [25]", 
                "Not supported [26]" 
        };


        List<MapiRecipDesc> m_recipients    = new 
            List<MapiRecipDesc>();
        List<string> m_attachments    = new List<string>();
        int m_lastError = 0;

        const int MAPI_LOGON_UI = 0x00000001;
        const int MAPI_DIALOG = 0x00000008;
        const int maxAttachments = 20;

        enum HowTo{MAPI_ORIG=0, MAPI_TO, MAPI_CC, MAPI_BCC};
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public class MapiMessage
    {
        public int reserved;
        public string subject;
        public string noteText;
        public string messageType;
        public string dateReceived;
        public string conversationID;
        public int flags;
        public IntPtr originator;
        public int recipCount;
        public IntPtr recips;
        public int fileCount;
        public IntPtr files;
    }

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

    [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]
    public class MapiRecipDesc
    {
        public int        reserved;
        public int        recipClass;
        public string    name;
        public string    address;
        public int        eIDSize;
        public IntPtr    entryID;
    }
}

Here's the code for the C# class. If you compare it to the C++ version in my previous article, you'll see it's basically the same structure with a few additions to cope with the marshaling.

Sidebar

By managing its own memory, you can't just include the mapi.h in your .NET code to get the necessary MapiMessage and MapiFileDesc structures. I've just hand copied the ones I needed, but maybe someone will complete this chore or write a .h converter.

The VB.NET way

Here's the equivalent VB.NET class which doesn't really need any more explanation than given above:

VB
Namespace SendFileTo
    Class MAPI
        Public Function AddRecipientTo(ByVal email As String) As Boolean
            Return AddRecipient(email, howTo.MAPI_TO)
        End Function

        Public Function AddRecipientCC(ByVal email As String) As Boolean
            Return AddRecipient(email, howTo.MAPI_TO)
        End Function

        Public Function AddRecipientBCC(ByVal email As String) As Boolean
            Return AddRecipient(email, howTo.MAPI_TO)
        End Function

        Public Sub AddAttachment(ByVal strAttachmentFileName As String)
            m_attachments.Add(strAttachmentFileName)
        End Sub

        Public Function SendMailPopup(ByVal strSubject As String, 
                ByVal strBody As String) As Integer
            Return SendMail(strSubject, strBody, MAPI_LOGON_UI Or MAPI_DIALOG)
        End Function

        Public Function SendMailDirect(ByVal strSubject As String, 
            ByVal strBody As String) As Integer
            Return SendMail(strSubject, strBody, MAPI_LOGON_UI)
        End Function


        <DllImport("MAPI32.DLL")> _
         Private Shared Function MAPISendMail(ByVal sess As IntPtr, 
             ByVal hwnd As IntPtr, ByVal message As MapiMessage, 
             ByVal flg As Integer, ByVal rsv As Integer) As Integer
        End Function

        Private Function SendMail(ByVal strSubject As String, 
            ByVal strBody As String, ByVal how As Integer) As Integer
            Dim msg As MapiMessage = New MapiMessage()
            msg.subject = strSubject
            msg.noteText = strBody

            msg.recips = GetRecipients(msg.recipCount)
            msg.files = GetAttachments(msg.fileCount)

            m_lastError = MAPISendMail(New IntPtr(0), New IntPtr(0), msg, how, 
                0)
            If m_lastError > 1 Then
                MessageBox.Show("MAPISendMail failed! " + GetLastError(), 
                    "MAPISendMail")
            End If

            Cleanup(msg)
            Return m_lastError
        End Function

        Private Function AddRecipient(ByVal email As String, 
            ByVal howTo As howTo) As Boolean
            Dim recipient As MapiRecipDesc = New MapiRecipDesc()

            recipient.recipClass = CType(howTo, Integer)
            recipient.name = email
            m_recipients.Add(recipient)

            Return True
        End Function

        Private Function GetRecipients(ByRef recipCount As Integer) As IntPtr
            recipCount = 0
            If m_recipients.Count = 0 Then
                Return 0
            End If

            Dim size As Integer = Marshal.SizeOf(GetType(MapiRecipDesc))
            Dim intPtr As IntPtr = Marshal.AllocHGlobal(
                m_recipients.Count * size)

            Dim ptr As Integer = CType(intPtr, Integer)
            Dim mapiDesc As MapiRecipDesc
            For Each mapiDesc In m_recipients
                Marshal.StructureToPtr(mapiDesc, CType(ptr, IntPtr), False)
                ptr += size
            Next

            recipCount = m_recipients.Count
            Return intPtr
        End Function

        Private Function GetAttachments(ByRef fileCount As Integer) As IntPtr
            fileCount = 0
            If m_attachments Is Nothing Then
                Return 0
            End If

            If (m_attachments.Count <= 0) Or (m_attachments.Count > 
                maxAttachments) Then
                Return 0
            End If

            Dim size As Integer = Marshal.SizeOf(GetType(MapiFileDesc))
            Dim intPtr As IntPtr = Marshal.AllocHGlobal(
                m_attachments.Count * size)

            Dim mapiFileDesc As MapiFileDesc = New MapiFileDesc()
            mapiFileDesc.position = -1
            Dim ptr As Integer = CType(intPtr, Integer)

            Dim strAttachment As String
            For Each strAttachment In m_attachments
                mapiFileDesc.name = Path.GetFileName(strAttachment)
                mapiFileDesc.path = strAttachment
                Marshal.StructureToPtr(mapiFileDesc, CType(ptr, IntPtr), False)
                ptr += size
            Next

            fileCount = m_attachments.Count
            Return intPtr
        End Function

        Private Sub Cleanup(ByRef msg As MapiMessage)
            Dim size As Integer = Marshal.SizeOf(GetType(MapiRecipDesc))
            Dim ptr As Integer = 0

            If msg.recips <> IntPtr.Zero Then
                ptr = CType(msg.recips, Integer)
                Dim i As Integer
                For i = 0 To msg.recipCount - 1 Step i + 1
                    Marshal.DestroyStructure(CType(ptr, IntPtr), 
                        GetType(MapiRecipDesc))
                    ptr += size
                Next
                Marshal.FreeHGlobal(msg.recips)
            End If

            If msg.files <> IntPtr.Zero Then
                size = Marshal.SizeOf(GetType(MapiFileDesc))

                ptr = CType(msg.files, Integer)
                Dim i As Integer
                For i = 0 To msg.fileCount - 1 Step i + 1
                    Marshal.DestroyStructure(CType(ptr, IntPtr), 
                        GetType(MapiFileDesc))
                    ptr += size
                Next
                Marshal.FreeHGlobal(msg.files)
            End If

            m_recipients.Clear()
            m_attachments.Clear()
            m_lastError = 0
        End Sub

        Public Function GetLastError() As String
            If m_lastError <= 26 Then
                Return errors(m_lastError)
            End If
            Return "MAPI error [" + m_lastError.ToString() + "]"
        End Function

        ReadOnly errors() As String = New String() {"OK [0]", "User abort [1]",
            "General MAPI failure [2]", "MAPI login failure [3]", 
            "Disk full [4]", "Insufficient memory [5]", "Access denied [6]", 
            "-unknown- [7]", "Too many sessions [8]", 
            "Too many files were specified [9]", 
            "Too many recipients were specified [10]", 
            "A specified attachment was not found [11]", 
            "Attachment open failure [12]", "Attachment write failure [13]", 
            "Unknown recipient [14]", "Bad recipient type [15]", 
            "No messages [16]", "Invalid message [17]", "Text too large [18]", 
            "Invalid session [19]", "Type not supported [20]", 
            "A recipient was specified ambiguously [21]", 
            "Message in use [22]", "Network failure [23]", 
            "Invalid edit fields [24]", "Invalid recipients [25]", 
            "Not supported [26]"}

        Dim m_recipients As New List(Of MapiRecipDesc)
        Dim m_attachments As New List(Of String)
        Dim m_lastError As Integer = 0

        Private Const MAPI_LOGON_UI As Integer = &H1
        Private Const MAPI_DIALOG As Integer = &H8
        Private Const maxAttachments As Integer = 20

        Enum howTo
            MAPI_ORIG = 0
            MAPI_TO
            MAPI_CC
            MAPI_BCC
        End Enum

    End Class

    <StructLayout(LayoutKind.Sequential)> _
    Public Class MapiMessage
        Public reserved As Integer
        Public subject As String
        Public noteText As String
        Public messageType As String
        Public dateReceived As String
        Public conversationID As String
        Public flags As Integer
        Public originator As IntPtr
        Public recipCount As Integer
        Public recips As IntPtr
        Public fileCount As Integer
        Public files As IntPtr
    End Class

    <StructLayout(LayoutKind.Sequential)> _
    Public Class MapiFileDesc
        Public reserved As Integer
        Public flags As Integer
        Public position As Integer
        Public path As String
        Public name As String
        Public type As IntPtr
    End Class

    <StructLayout(LayoutKind.Sequential)> _
    Public Class MapiRecipDesc
        Public reserved As Integer
        Public recipClass As Integer
        Public name As String
        Public address As String
        Public eIDSize As Integer
        Public enTryID As IntPtr
    End Class
End Namespace

Example use

The attached C# and VB.NET samples show the whole story, but to use this is pretty easy as this fragment shows:

Screenshot - SendToNET3.jpg

C#
using SendFileTo;

namespace TestSendTo
{
    public partial class Form1 : Form
    {
        private void btnSend_Click(object sender, EventArgs e)
        {
            MAPI mapi = new MAPI();

            mapi.AddAttachment("c:\\temp\\file1.txt");
            mapi.AddAttachment("c:\\temp\\file2.txt");
            mapi.AddRecipientTo("person1@somewhere.com");
            mapi.AddRecipientTo("person2@somewhere.com");
            mapi.SendMailPopup("testing", "body text");
            
            // Or if you want try and do a direct send without displaying the 
            // mail dialog mapi.SendMailDirect("testing", "body text");
        }
    }
}

You can call AddAttachment and AddRecipient multiple times to add as many attachments and recipients as you want, although I've put an arbitrary limit of 20 on the attachments just as a precaution. Calling SendMailPopup will display the email client send mail dialog and you can then add further recipients or attachments and hit the send button manually. Alternatively, you can call SendMailDirect which will attempt to send the mail without showing the email client dialog.

Make sure when using the samples with hard coded attachment paths shown above that the files exist in the given locations. If you don't, SendMail will fail.

Errors

If the return value from the underlying MAPISendMail is anything other than OK[0] or User aborted[1], a message box will be displayed with the appropriate error text and code.

I cannot self-terminate. You must lower me into the steel.

Hopefully, this has answered all the questions although I'm sure there will be comments. I've been using the C# code in a few applications for remote bug reporting, and I haven't had any problems. Do let me know if you spot any howlers. I hope there aren't any, otherwise I'll have to stoop to an unworthy Rise Of The Email Attachments.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)