Open default mail client along with a attachment
Asked Answered
C

5

36

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

Condyle answered 2/12, 2013 at 12:40 Comment(3)
If someone needs it for asp.net: #45607527Drambuie
Duplicate: https://mcmap.net/q/219814/-c-mailto-with-attachment/5389585Collin
Related: html - using mailto to send email with an attachment - Stack Overflow .Firewarden
T
64

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("[email protected]");
        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);
Thora answered 30/8, 2014 at 20:3 Comment(11)
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)Brynhild
This is not working, it throws exception on line: sendMethod.Invoke(message, BindingFlags.Instance | .......Dees
not clear how to delete generated "mymessage.eml" fileChromous
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.Fuld
Does this method work with all the email clients? Or at least with the most popular ones?Duet
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?Paiz
@Paiz 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?Rhianna
Windows 10 mail doesn't seem to consume the X-Unsent header and just opens it as a non-outgoing emailMadelyn
The .eml file is not editable. How to make this editable?Japheth
I had to change the it to this: sendMethod.Invoke(message, BindingFlags.Instance | BindingFlags.NonPublic, null, new[] { mailWriter, true }, null);Tallman
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.Dewey
W
12

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

mailto:[email protected]?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.

Wastage answered 2/12, 2013 at 12:46 Comment(4)
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.Worldshaking
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.Condyle
@Condyle In future, eliminating the obvious answer that you already tried would be a big plus :DWastage
@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.Condyle
W
5

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

var url = "mailto:[email protected]";
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:[email protected]?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.

Worldshaking answered 2/12, 2013 at 12:46 Comment(3)
Unfortunately, the OP asked specifically about attachments.Driving
It's not a perfect answer for the OP. But it did what I needed. :)Vaduz
It is just opening a blank page in the Chrome browser.Lining
S
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("[email protected]");
mapi.AddRecipientTo("[email protected]");
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

Samba answered 18/9, 2015 at 11:26 Comment(4)
the way doesn't work on my environment (W7, VS2013, executed from test): no error (code=0), no opened OutlookChromous
same problem here, this code does not work. Win 10, VS2010, no error, nothing opened.Abijah
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.Algology
It seems like To, CC and BCC do all the same thing.Bootee
M
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(
    "[email protected]",
    "[email protected]",
    "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);
}
Mainstay answered 1/4, 2023 at 3:54 Comment(1)
Relative folders are not supported for me in .NET 4.8, apart from that: thank you!Millennial

© 2022 - 2024 — McMap. All rights reserved.