35

Hi I am working on a WPF application (using c#).

I need to have a functionality where users can send files (audio files) as attachments via email. I tried using Microsoft.Office.Interop.Outlook.Application namespace but it opens outlook and wont work if outlook is not installed on the client's computer.

I tried using SmtpClient() and MailMessage() classes of System.Net.Mail namespace but its not opening email client. Its sending a mail through predefined server (might be a problem since I don't know what my client's default email domain is. This link has all the things I need and its working fine.

But there they used DllImport attribute and there are many issues that may arise (from what I can understand) from using this method. I have no idea about managed and un-managed code so I am not able to understand what the problem is. Is it OK to follow the example in the above link. If not why?

Can you tell or provide links on how to approach my problem

huMpty duMpty
  • 14,346
  • 14
  • 60
  • 99
Raj123
  • 971
  • 2
  • 12
  • 24

5 Answers5

63

We can make use of the fact that most email clients support the .EML file format to be loaded.

So if we Extend the System.Net.Mail.MailMessage Class in a way that it can be saved to the filesystem as an .EML file. The resulting file can be opened with the default mail client using Process.Start(filename)

For this to work properly we have to add a line containing "X-Unsent: 1" to the .EML file. This line tells the email client loading the .EML file the message must be presented in "New message" mode.

Use the "addUnsentHeader" bool parameter of the extension method to add this line to the .EML file

The extension method looks like this:

using System;
using System.IO;
using System.Net.Mail;
using System.Reflection;

namespace Fsolutions.Fbase.Common.Mail
{
    public static class MailUtility
    {
        //Extension method for MailMessage to save to a file on disk
        public static void Save(this MailMessage message, string filename, bool addUnsentHeader = true)
        {
            using (var filestream = File.Open(filename, FileMode.Create))
            {
                if (addUnsentHeader)
                {
                    var binaryWriter = new BinaryWriter(filestream);
                    //Write the Unsent header to the file so the mail client knows this mail must be presented in "New message" mode
                    binaryWriter.Write(System.Text.Encoding.UTF8.GetBytes("X-Unsent: 1" + Environment.NewLine));
                }

                var assembly = typeof(SmtpClient).Assembly;
                var mailWriterType = assembly.GetType("System.Net.Mail.MailWriter");

                // Get reflection info for MailWriter contructor
                var mailWriterContructor = mailWriterType.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, new[] { typeof(Stream) }, null);

                // Construct MailWriter object with our FileStream
                var mailWriter = mailWriterContructor.Invoke(new object[] { filestream });

                // Get reflection info for Send() method on MailMessage
                var sendMethod = typeof(MailMessage).GetMethod("Send", BindingFlags.Instance | BindingFlags.NonPublic);

                sendMethod.Invoke(message, BindingFlags.Instance | BindingFlags.NonPublic, null, new object[] { mailWriter, true, true }, null);

                // Finally get reflection info for Close() method on our MailWriter
                var closeMethod = mailWriter.GetType().GetMethod("Close", BindingFlags.Instance | BindingFlags.NonPublic);

                // Call close method
                closeMethod.Invoke(mailWriter, BindingFlags.Instance | BindingFlags.NonPublic, null, new object[] { }, null);
            }
        }
    }
}

Use the extension method like this:

        var mailMessage = new MailMessage();
        mailMessage.From = new MailAddress("someone@yourdomain.com");
        mailMessage.Subject = "Your subject here";
        mailMessage.IsBodyHtml = true;
        mailMessage.Body = "<span style='font-size: 12pt; color: red;'>My HTML formatted body</span>";

        mailMessage.Attachments.Add(new Attachment("C://Myfile.pdf"));

        var filename = "C://Temp/mymessage.eml";

        //save the MailMessage to the filesystem
        mailMessage.Save(filename);

        //Open the file with the default associated application registered on the local machine
        Process.Start(filename);
Andrius Naruševičius
  • 8,348
  • 7
  • 49
  • 78
Williwyg
  • 651
  • 5
  • 3
  • I think this is the best workaround even if some mail-clients don't support the X-Unsent header (for example thunderbird, but there is an addon that permits it to handle this) – Tobia Zambon Nov 26 '14 at 09:49
  • 4
    This is not working, it throws exception on line: sendMethod.Invoke(message, BindingFlags.Instance | ....... – Marjan Slavkovski Jan 24 '15 at 15:51
  • not clear how to delete generated "mymessage.eml" file – constructor Nov 09 '15 at 09:42
  • Using strings to reflect into assemblies you don't own is probably a bad idea. If those members' names ever change, your code will break. – Wai Ha Lee Nov 18 '15 at 12:50
  • Does this method work with all the email clients? Or at least with the most popular ones? – Pincopallino May 05 '16 at 19:46
  • 1
    I'm getting the same error as @Marjan - but I have tried implementing this as all one "chunk" (rather than extending `System.Net.Mail.MailMessage`). What is the proper implementation of this solution? – derekantrican Aug 18 '16 at 14:39
  • 3
    @derekantrican this method works when you know the from address, the mailMessage.From must be set before mailMessage.Save, so it seems that we should get the account from the default email client first when using this method? – Bos Sep 23 '16 at 08:33
  • 4
    Windows 10 mail doesn't seem to consume the X-Unsent header and just opens it as a non-outgoing email – Chuck Pinkert Dec 06 '16 at 17:56
  • 1
    The .eml file is not editable. How to make this editable? – Rich Jul 18 '17 at 11:47
  • I had to change the it to this: sendMethod.Invoke(message, BindingFlags.Instance | BindingFlags.NonPublic, null, new[] { mailWriter, true }, null); – dvdhns Mar 12 '19 at 17:22
  • 3
    We tried to used this code as is, but for some reason our Outlook environment would not let us send the email. We got a failure from our Exchange server that said "You can't send a message on behalf of this user unless you have permission to do so." If we manually select the From address in Outlook (even if the from in the .eml was the same) then the email would go through. So, our solution is that we stripped the "From" line in the .eml after running the above code, and then the email would open up without a predefined "From", and allow us to send. – A. Blodgett May 07 '19 at 03:52
11

Have you tried using System.Diagnostics.Process.Start() with an appropriate command line?

mailto:some.guy@someplace.com?subject=an email&body=see attachment&attachment="/files/audio/attachment.mp3"

The &attachment switch lets you specify a file name.

Ok, I'm struggling to this working but allegedly it can be done. I'm currently reading through this monster and will get back to you.

Gusdor
  • 14,001
  • 2
  • 52
  • 64
  • 9
    I have not been able to get my e-mail client to handle `attachment` and as I have pointed out RFC 6068 does not define any support for attachments. – Martin Liversage Dec 02 '13 at 13:13
  • I forgot to mention this. I tried using mailto... method also but what it does is its opening a new tab in my browser. When trying it from a friends computer Outlook is opening but there is no attachement. – Raj123 Dec 02 '13 at 13:28
  • @Raj123 In future, eliminating the obvious answer that you already tried would be a big plus :D – Gusdor Dec 02 '13 at 14:09
  • @Gudsor I tried mailto method but I was under the impression that attachments are not possible with this method. I tried the way you suggested but as I said before a new tab is opening in my browser (I don't know what's the relation between mailto and my browser). I am going through the link you provided but I think there might be some fault with my computer as other systems are opening their default mail client. – Raj123 Dec 03 '13 at 05:13
5

You can ask the Windows shell to open a mailto URL:

var url = "mailto:someone@somewhere.com";
Process.Start(url);

You need to be using System.Diagnostics.

You can set various parts of the message like subject and body as described in RFC 6068

var url = "mailto:someone@somewhere.com?subject=Test&body=Hello";

Unfortunately, the mailto protocol does not support attachments even though some e-mail clients may have a way of handling that.

Martin Liversage
  • 104,481
  • 22
  • 209
  • 256
1

I used the following helper class.

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;
}

Use the MAPI class as below.

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");

Reference: Code Project

Gopichandar
  • 2,742
  • 2
  • 24
  • 54
  • the way doesn't work on my environment (W7, VS2013, executed from test): no error (code=0), no opened Outlook – constructor Nov 09 '15 at 09:53
  • same problem here, this code does not work. Win 10, VS2010, no error, nothing opened. – Richard S. Jul 07 '16 at 19:40
  • 1
    We are in production with MAPI and I do not recommend it at all. It causes issues with HTML Mails and often throws errors if something is not exactly as it should be, leaving you searching for registry fixes that rarely work. We're trying the EML approach now. – LueTm Nov 27 '17 at 16:55
  • It seems like To, CC and BCC do all the same thing. – Thomas Weller Dec 11 '17 at 14:21
1

You can use SmtpDeliveryMethod.SpecifiedPickupDirectory to have the Send function send it to a specific folder as a EML file. Then you can edit said file to make it behave how you wish. Lastly use Process.Start on the file path.

Example:

// cleanup a temp folder to hold email. This make sit easy to find the file that is created
string tempEmailDirectory = ".\\Email Temp";
if (Directory.Exists(tempEmailDirectory))
{
    DirectoryInfo emailDirectory = new DirectoryInfo(tempEmailDirectory);
    foreach (FileInfo file in emailDirectory.GetFiles())
    {
        file.Delete();
    }
}
else
{
    Directory.CreateDirectory(tempEmailDirectory);
}

// send email to folder on harddrive
using MailMessage email = new MailMessage(
    "from@email.com",
    "to@email.com",
    "subject",
    "body");

email.Attachments.Add(new Attachment("attachment.png");

using SmtpClient smtp = new SmtpClient();
smtp.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory;
smtp.PickupDirectoryLocation = tempEmailDirectory;
smtp.Send(email);

email.Attachments.Dispose();

// now find the file that was created so we can modify it
string[] emails = Directory.GetFiles(tempEmailDirectory, "*.eml");
if (emails.Length > 0)
{
    string emlFilePath = emails[0];
    
    var emlText = File.ReadAllText(emlFilePath);
    
    // replace from: line with the unsent tag
    // this way it will open a window for you to send it from your local email address
    emlText = Regex.Replace(emlText, "From: .*?\r\n", "X-Unsent: 1\r\n");

    using (var fileStream = File.Open(emlFilePath, FileMode.Create))
    {
        using var binaryWriter = new BinaryWriter(fileStream);
        binaryWriter.Write(System.Text.Encoding.UTF8.GetBytes(emlText));
    }
    
    // open in email program
    Process.Start(emlFilePath);
}