Search code examples
c#attachmentsmtpclient

How to add file attachments to .NET SmtpClient MailMessage email from a form submission


I have an MVC form which has 3 file input fields on it. If those input fields have values on them, I want to add them as an attachment to a notification email. Please note that in the example below addedFiles is a HttpFileCollectionBase.

        var smtpServer = Sitecore.Configuration.Settings.GetSetting("MailServer");
        var smtpPort = Sitecore.Configuration.Settings.GetSetting("MailServerPort");
        using (var stream = new MemoryStream())
        using (var mailClient = new SmtpClient(smtpServer, Convert.ToInt16(smtpPort)))
        using (var emailMessage = new MailMessage(fromAddress, toAddress, subject, message))
        {
            if (addedFiles != null && addedFiles.Count > 0)
            {
                //for some reason, the first file field was getting repeated at the end.  Workaround.
                for (int i = 0; i < 3; i++)
                {
                    string fileName = addedFiles.Keys[i];
                    HttpPostedFileBase file = addedFiles[fileName];
                    if ((file.FileName.Contains(".pdf") ||
                        file.FileName.Contains(".doc")) && file.ContentLength > 0 && file.ContentLength < 10485760)
                    {

                        var fStream = file.InputStream;
                        fStream.Position = 0;
                        fStream.CopyTo(stream);
                        var s = stream.ToArray();
                        stream.Write(s, 0, file.ContentLength);
                        stream.Position = 0; 

                        emailMessage.Attachments.Add(new Attachment(stream, file.FileName));


                    }
                }
                    await Task.Run(() => mailClient.Send(emailMessage));                  
            }
        }

What is currently happening is the email is generated and the files do get attached. The size of the file is correct in the email attachment (if not a few KB larger than the original). However, upon attempting to open the file, I get a message that it is corrupted. The test file is a .docx file. I have tested the original file to ensure it is not corrupted and I am able to open it so I do know it's not the file. I'm sure I'm missing something silly. Just need a little guidance.

UPDATE

The issue is only with docx files. Pdf and doc files are fine. I'm not sure why only docx file come through as corrupted. Any ideas?


Solution

  • You're using the Streams incorrectly. You need to use a separate Stream for each Attachment.

    As a neat trick, (I believe) you don't need an intermediate stream or buffer - but you can pass the file-upload streams directly to the Attachment constructors provided the MailMessage will be sent before the ASP.NET Request/Response lifecycle ends. (Note that Attachment takes ownership of the stream passed into its constructor, so you don't need to dispose of the attachment's stream yourself provided that the parent MailMessage is also disposed).

    Also there's a few things that don't look right in your code (such as hardcoding 3 for the number of files) and doing await Task.Run( ... ) for a non-async operation.

    As you're using the System.Web version of ASP.NET (i.e. not using ASP.NET Core) I don't recommend using any async APIs because that messes with the request/response lifecycle.

    Try this:

    HttpFileCollectionBase addedFiles = ...
    using( SmtpClient  mailClient = new SmtpClient( smtpServer, Convert.ToInt16( smtpPort ) ) )
    using( MailMessage emailMessage = new MailMessage( fromAddress, toAddress, subject, message ) )
    {
        if( addedFiles?.Count > 0 )
        {
            foreach( HttpPostedFileBase file in addedFiles )
            {
                Boolean isOK = ( file.FileName.EndsWith( ".pdf", StringComparison.OrdinalIgnoreCase ) || file.FileName.EndsWith( ".doc", StringComparison.OrdinalIgnoreCase ) ) && file.ContentLength > 0 && file.ContentLength < 10485760;
                if( isOK )
                {
                    Attachment att = new Attachment( file.InputStream, name: file.FileName );
                    emailMessage.Attachments.Add( att );
                } 
            }
        }
    
        mailClient.Send( emailMessage );
    }
    

    If you do need to have the MailMessage outlast the ASP.NET request/response lifecycle, or if you want to inspect or process the uploaded files before attaching them, then you'll need to buffer them individually, like so:

    HttpFileCollectionBase addedFiles = ...
    using( SmtpClient  mailClient = new SmtpClient( smtpServer, Convert.ToInt16( smtpPort ) ) )
    using( MailMessage emailMessage = new MailMessage( fromAddress, toAddress, subject, message ) )
    {
        if( addedFiles?.Count > 0 )
        {
            foreach( HttpPostedFileBase file in addedFiles )
            {
                Boolean isOK = ( file.FileName.EndsWith( ".pdf", StringComparison.OrdinalIgnoreCase ) || file.FileName.EndsWith( ".doc", StringComparison.OrdinalIgnoreCase ) ) && file.ContentLength > 0 && file.ContentLength < 10485760;
                if( isOK )
                {
                    MemoryStream copy = new MemoryStream( capacity: file.ContentLength );
                    file.InputStream.CopyTo( copy );
                    // Rewind the stream, this is important! (You cannot rewind ASP.NET's file.InputStream, hence why we use a MemoryStream copy).
                    copy.Seek( 0, SeekOrigin.Begin );
    
                    DoSomethingWithFileStream( copy );
    
                    // Rewind the stream again, this is important!
                    copy.Seek( 0, SeekOrigin.Begin );
    
                    Attachment att = new Attachment( copy, name: file.FileName );
                    emailMessage.Attachments.Add( att );
                } 
            }
        }
    
        mailClient.Send( emailMessage );
    }