By default, the .Net mail classes in System.Web.Mail offer very weak functionality for composing the message format. On the other hand, CDONTS, the "predecessor" to System.Web.Mail is incredibly rich in functionality and very powerful. I use "predecessor" in quotes because, as some may be surprised to learn, System.Web.Mail actually uses CDONTS; System.Web.Mail is not a new mail library, but rather a very limited/severely crippled interface to CDO. You can confirm this for yourself by using Reflector on the System.Web.Mail.SmtpMail class.
Well, if you're satisfied with what's offered in System.Web.Mail or you're already using a third party mail package, then read no further. If you'd like to find out how to take advangate of CDONTS to maximize your flexibility when working with emails in .Net, then this workshop is for you :-) Bear in mind that even the all powerful CDONTS has its limitations (I've had to write a custom SMTP client once, but that's for another workshop), but it is far more powerful than the default classes in .Net. Among the cool things that you can do with CDONTS:
This last bullet is the subject of this workshop. Using CDONTS, it's possible to send an email with a user uploaded attachment without saving the attachment to disk first.
If you peek at the MSDN documentation for System.Web.Mail.MailAttachment, you'll see that both constructors require a filename for an attachment.
In some scenarios, if you're sending mail from an ASPX page, you may not want to/need to write the file to the disk first (space constraints, sensitive files, no control of disk write access, etc.). Not to mention that it's a double whammy in terms of performance since you need to write the POST'd file to disk, and then simply turn around and read the file again! To work around this we will utilize CDONTS and good old ADODB, via .Net interops, to manually build and send an email message with an attachment without saving the attachment to disk first.
To try this out for yourself, you will need the following:
I will assume that anyone that's still reading this knows how to create VB.Net web projects in Visual Studio.
In VS.Net, create a new web project (I'm using VB.Net for this workshop, but it's trivial if you would prefer C#).
We will need to add a reference to CDONTS/CDOSYS. In the "COM" tab, scroll down to the "M" letters and look for "Microsoft CDO for Windows 2000 Library". You'll note that "Microsoft CDO for NTS 1.2 Library" is right above it. Adding the former will add the "CDO" namespace to your project while adding the latter will add the "CDONTS" namespace to your project. For the purposes of this project, even though I've been using "CDONTS", we will actually be using CDOSYS. On my machine, the path is C:\WINDOWS\System32\cdosys.dll

Note that adding the CDOSYS reference automatically adds the ADODB reference for you. When you're all done, your references should look like so:

Now that we've added the references, we need to go ahead and build our input form. If you'd like to use my code verbatim, then you should delete the WebForm1.aspx file and add a new web form called Default.aspx.
We will need 5 controls on the page:
All of these are optional if you don't want to have dynamic input. You can hardcode all of these values. In my sample, I will also be adding some validators as well. Here's my ASPX page:
<%@ Page Language="vb"
AutoEventWireup="false"
Codebehind="Default.aspx.vb"
Inherits="_Default"
Trace="true"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Email Attachment Sample/Test</title>
<style type="text/css">
* {
font-size: 11px;
color: #333;
font-family: verdana
}
div.content {
border: #999 1px solid;
padding: 3px;
background: #eee;
margin: 2px;
width: 500px;
}
div.center {
padding: 4px;
text-align: center
}
p {
margin: 2px 0px;
}
p.heading {
font-style:oblique;
font-weight:bold;
margin-top:5px;
}
</style>
</head>
<body >
<!--///
THE FORM ELEMENT MUST HAVE THE enctype ATTRIBUTE
AND IT MUST BE SET TO "multipart/form-data"
///-->
<form id="Form1" method="post" runat="server"
enctype="multipart/form-data">
<div class="content">
<p><asp:Label
ID="ErrorLabel"
Runat="server"
Visible="False"
style="color:red"
EnableViewState="False"/>
<asp:Label
ID="SuccessLabel"
Runat="server"
Visible="False"
style="color:green"
EnableViewState="False"/></p>
<p class="heading">Select a file to upload:</p>
<!--///
FILE INPUT
///-->
<p><input
type="file"
id="FileUploadBox"
runat="server"
style="width:400px"></p>
<p class="heading">Enter an email address:</p>
<!--///
EMAIL INPUT
///-->
<p><asp:TextBox
Id="EmailTextBox"
Runat="server"
Width="400">yourself@yourdomain.com
</asp:TextBox>
<asp:RequiredFieldValidator
Id="EmailRequiredValidator"
Runat="server"
Display="Dynamic"
ErrorMessage=" * Required"
ControlToValidate="EmailTextBox"/>
<asp:RegularExpressionValidator
Id="EmailExpressionValidator"
Runat="server"
Display="Dynamic"
ErrorMessage=" * Invalid"
ControlToValidate="EmailTextBox"
ValidationExpression="(?:\w|\.|-)+@(?:\w|-)+\.[a-zA-Z-_\.]+"/></p>
<p class="heading">"From" email address:</p>
<!--///
"FROM" EMAIL INPUT (NEEDS TO BE A VALID EMAIL ADDRESS)
///-->
<p><asp:TextBox
Id="FromEmailTextBox"
Runat="server"
Width="400">yourself@yourdomain.com
</asp:TextBox>
<asp:RequiredFieldValidator
Id="FromRequiredValidator"
Runat="server"
Display="Dynamic"
ErrorMessage=" * Required"
ControlToValidate="FromEmailTextBox"/>
<asp:RegularExpressionValidator
Id="FromExpressionValidator"
Runat="server"
Display="Dynamic"
ErrorMessage=" * Invalid"
ControlToValidate="FromEmailTextBox"
ValidationExpression="(?:\w|\.|-)+@(?:\w|-)+\.[a-zA-Z-_\.]+"/></p>
<p class="heading">Subject:</p>
<!--///
THE "SUBJECT" OF THE EMAIL
///-->
<p><asp:TextBox
Id="SubjectTextBox"
Runat="server"
Width="400">Here is my document.
</asp:TextBox>
<asp:RequiredFieldValidator
Id="SubjectRequiredValidator"
Runat="server"
Display="Dynamic"
ErrorMessage=" * Required"
ControlToValidate="SubjectTextBox"/></p>
<p class="heading">Message:</p>
<!--///
THE "MESSAGE" OF THE EMAIL
///-->
<p><asp:TextBox
Id="MessageTextbox"
Runat="server"
TextMode="MultiLine"
Width="400" Height="100">Please read the document.
</asp:TextBox>
<asp:RequiredFieldValidator
Id="MessageRequiredValidator"
Runat="server"
Display="Dynamic"
ErrorMessage=" * Required"
ControlToValidate="MessageTextbox"/></p>
<!--/// SUBMIT BUTTON ///-->
<div class="center">
<asp:Button
Id="SubmitButton"
Runat="server"
Text="Submit"/>
</div>
</div>
</form>
</body>
</html>If you're using my ASPX markup, you'll also need to add the controls to the codebehind. Here is my skeleton codebehind:
Imports System.Configuration Imports System.IO Imports System.Web.UI.WebControls Imports System.Web.UI.HtmlControls Imports CDO Imports ADODB Public Class _Default Inherits System.Web.UI.Page Protected WithEvents EmailTextBox As TextBox Protected WithEvents FromEmailTextBox As TextBox Protected WithEvents SubmitButton As Button Protected WithEvents SubjectTextBox As TextBox Protected WithEvents MessageTextbox As TextBox Protected WithEvents ErrorLabel As Label Protected WithEvents SuccessLabel As Label Protected WithEvents EmailRequiredValidator As RequiredFieldValidator Protected WithEvents EmailExpressionValidator As RegularExpressionValidator Protected WithEvents FromRequiredValidator As RequiredFieldValidator Protected WithEvents FromExpressionValidator As RegularExpressionValidator Protected WithEvents SubjectRequiredValidator As RequiredFieldValidator Protected WithEvents MessageRequiredValidator As RequiredFieldValidator Protected WithEvents FileUploadBox As HtmlInputFile '//------------------------------------------------------------------------ '// Page load '//------------------------------------------------------------------------ Private Sub Page_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load '// NOTHING TO DO. End Sub #Region " Web Form Designer Generated Code " 'This call is required by the Web Form Designer. <System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent() End Sub Private Sub Page_Init(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Init 'CODEGEN: This method call is required by the Web Form Designer 'Do not modify it using the code editor. InitializeComponent() End Sub #End Region End Class
In the codebeind you will need to add the reference to ADODB and CDO in the Imports at the top of the page.
Now that we've got the main form and codebehind set up, we need to just quickly add our function stubs. For this example, we will need three functions:
Your stubs should look like so:
'//------------------------------------------------------------------------
'// Handles the click event of the submit button.
'// 1. Checks that the uploaded file exists and is valid
'// 2. Use CDO to attach the file without saving it to disk
'// 3. Send the email
'//------------------------------------------------------------------------
Private Sub SubmitButton_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles SubmitButton.Click
Throw New NotImplementedException("Not Implemented Yet!")
End Sub
'//------------------------------------------------------------------------
'// Helper function to check whether a file is "valid" based on a number of
'// criteria.
'//------------------------------------------------------------------------
'// [Inputs]
'// file : The file that was POST'ed to the server
'// message : Used to return a string message back to the caller
'// [Outputs]
'// (return) : A boolean value corresponding to whether the file is
'// "valid"
'//------------------------------------------------------------------------
Private Function ValidateFile(ByVal file As HttpPostedFile, _
ByRef message As String) As Boolean
Throw New NotImplementedException("Not Implemented Yet!")
End Function
'//------------------------------------------------------------------------
'// Helper function to check whether a file is "valid" based on a number of
'// criteria. Assumes that ValidateFile() has already been called on the
'// file and the return value is the Boolean value True. If ValidateFile
'// is not called first and/or the return value is thethe Boolean value
'// False, an exception may be thrown.
'//------------------------------------------------------------------------
'// [Inputs]
'// file : The file that was POST'ed to the server
'// [Outputs]
'// (return) : A string value which contains the file's full name (file
'// name and extension).
'//------------------------------------------------------------------------
Private Function GetFullFilename(ByVal file As HttpPostedFile) As String
Throw New NotImplementedException("Not Implemented Yet!")
End FunctionThe first step is to fill in the file validation function. Among the things we would like to validate before processing the file are:
Feel free to come up with your own validation checks. For the last two bullets, it would help to have these values defined in the configuraiton file. So before continuing, add the following keys into your congifuration file:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <!--/// MAXIMUM ALLOWED FILE SIZE IN BYTES ///--> <add key="MaxFileSize" value="524290"/> <!--/// FILE EXTENSIONS WHICH ARE NOT ALLOWED ///--> <add key="InvalidFileExtensions" value="exe,js,vbs,sql,bat,com,asp,aspx,pif,scr"/> <!--/// ADDRESS OF THE MAIL SERVER ///--> <add key="MailServer" value="mail.optonline.net"/> </appSettings> <system.web> <!--/// OVERRIDE THE SYSTEM DEFAULT SIZE LIMIT (AS NECESSARY). VALUE IS IN KBYTES ///--> <httpRuntime maxRequestLength="512"/> </system.web> </configuration>
Note that we also added a key for the mail server address and a key to the system.web node which overrides the default setting in machine.config. Note that the maxRequestLength value is in kilobytes while the MaxFileSize key is in bytes (I've set these to be equal in this case). In reality, you could probably use just the maxRequestLength setting, which would effectively disallow requests larger than 512 kilobytes.
Below is my implementation of the method:
Private Function ValidateFile(ByVal file As HttpPostedFile, _
ByRef message As String) As Boolean
'// CHECK THAT THE FILE EXISTS
If file Is Nothing Then
message = "The file does not exist or was not successfully "
message = message & "uploaded. Please try again."
Return False
End If
'// CHECK THAT THE FILE HAS A MINIMUM LENGTH GREATER THAN 0
If Not file.ContentLength > 0 Then
message = "The file had a content length of 0. Please try again."
Return False
End If
'// CHECK THAT THE FILE EXTENSION EXISTS
Dim position As Integer = file.FileName.LastIndexOf(".")
If position < 1 Then
message = "The file did not contain an extension. Please add a "
message = message & "valid extension to the file."
Return False
End If
'// CHECK THAT VALID EXTENSIONS ARE DEFINED
Dim invalidFileExtensions As String = _
ConfigurationSettings.AppSettings("InvalidFileExtensions")
If invalidFileExtensions Is Nothing Then
message = "The InvalidFileExtensions key is not defined in the "
message = message & "configuration file."
Return False
End If
'// CHECK THAT THE FILE EXTENSION IS VALID
Dim extension As String = file.FileName.Substring(position + 1)
If invalidFileExtensions.IndexOf(extension) > -1 Then
message = String.Format("The file extension {0} is not valid.", _
extension)
Return False
End If
'// CHECK THAT THE FILE SIZE LIMIT IS DEFINED
Dim maxFileSizeStr As String = _
ConfigurationSettings.AppSettings("MaxFileSize")
If maxFileSizeStr Is Nothing Then
message = "The MaxFileSize key is not defined in the "
message = message & "configuration file."
Return False
End If
Dim maxFileSize As Long = 0
Try
maxFileSize = Long.Parse(maxFileSizeStr)
Catch
message = "The MaxFileSize value defined was not numeric."
Return False
End Try
'// CHECK THE FILE SIZE LIMIT
If file.ContentLength > maxFileSize Then
message = "The size of the file submitted exceeded the maximum "
message = message & "allowable file size."
Return False
End If
'// ALL CHECKS PASSED
message = String.Empty
Return True
End FunctionNote my use of ByRef for the second argument. This is a shortcut that I'm using to return two values from the call. A better implementation would be to create a wrapper class like ValidationResult or something. Also note that I broke up the message strings, which is unnecessary; I'm only breaking them up for display purposes (I knew I should have made the body wider in the template).
Next, we want to fill in the helper function for retrieving the filename (including extension, excluding the full path) from the HttpPostedFile's FileName property. By default, the FileName property contains the full path and filename of the file from the source machine, thus this function is used to extract only the filename and extensio. Feel free to exclude this function and do it inline instead.
My implementation is as follows:
Private Function GetFullFilename(ByVal file As HttpPostedFile) As String
'// CAPTURE LOCATIONS OF BOTH BACK AND FORWARD SLASH TO ACCOMODATE
'// FILES LOADED FROM A NETWORK PATH.
Dim indexBackSlash As Integer = file.FileName.LastIndexOf("/")
Dim indexForwardSlash As Integer = file.FileName.LastIndexOf("\")
Dim fileName As String = String.Empty
If indexBackSlash >= 0 Then
fileName = file.FileName.Substring(indexBackSlash + 1)
ElseIf indexForwardSlash >= 0 Then
fileName = file.FileName.Substring(indexForwardSlash + 1)
Else
'// CANNOT PARSE THE FILE NAME PROPERLY
Throw New ArgumentException( _
String.Format("The filepath [{0}] is invalid.", _
file.FileName))
End If
Return fileName
End FunctionThe last step is to pull all of the pieces together and implement the event handler for the click event or our button.
A lot of what I ended up with was figured out through trial and error. The general idea behind this concept goes like this:
The real trick is in 3-8. Instead of saving the POST'ed file (now in some byte stream in memory) to the disk first, we use it directly by writing the stream contents, in base 64 encoding to the mail message object. You can figure most of this stuff out with some experimentation and the documenation available online. With some trial and error, I was able to figure out how to get this stuff to work properly.
My implementation is as follows:
Private Sub SubmitButton_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles SubmitButton.Click
'// CHECK THAT THE FILE IS VALID
Dim message As String = String.Empty
Dim file As HttpPostedFile = FileUploadBox.PostedFile
Page.Validate()
If Page.IsValid And ValidateFile(file, message) Then
'// DECLARE STREAMS
Dim stream1 As ADODB.Stream = Nothing
Dim stream2 As ADODB.Stream = Nothing
'// GET EMAIL CONTENT DATA
Dim recipientEmailAddress As String = EmailTextBox.Text
Dim senderEmailAddress As String = FromEmailTextBox.Text
Dim emailSubject As String = SubjectTextBox.Text
Dim emailMessage As String = MessageTextbox.Text
Try
'// FIRST STEP IS TO CONVERT THE UPLOADED FILE, WHICH IS STORED
'// AS AN IN-MEMORY BYTE STREAM, INTO A BYTE ARRAY.
Dim fileContents(file.InputStream.Length) As Byte
file.InputStream.Read(fileContents, 0, _
Convert.ToInt32(file.InputStream.Length))
'// DECLARE VARIABLES; USING CDO (FOR EXCHANGE) MESSAGE CLASSES
'// SO THAT WE CAN ATTACH FILE WITHOUT SAVING IT TO THE DISK.
Dim mail As CDO.Message = New CDO.MessageClass
Dim mailBody As CDO.IBodyPart = mail.BodyPart
Dim textBody As CDO.IBodyPart
Dim fileBody As CDO.IBodyPart
Dim config As CDO.Configuration = mail.Configuration
'// SET THE CONFIGURATION VALUES FOR THE MAIL MESSAGE
config.Fields(CdoConfiguration.cdoSendUsingMethod).Value = _
CdoSendUsing.cdoSendUsingPort
config.Fields(CdoConfiguration.cdoSMTPServer).Value = _
ConfigurationSettings.AppSettings("MailServer")
config.Fields(CdoConfiguration.cdoSMTPServerPort).Value = 25
config.Fields.Update()
'// CREATE MULTIPART MESSAGE (LEVEL 0)
mailBody.ContentMediaType = "multipart/mixed"
'// BUILD TEXT MESSAGE BODY PART MESSAGE BODY (LEVEL 1)
textBody = mailBody.AddBodyPart(1)
textBody.ContentMediaType = "text/plain"
textBody.ContentTransferEncoding = "7bit"
stream1 = textBody.GetDecodedContentStream()
stream1.WriteText(emailMessage, StreamWriteEnum.stWriteLine)
stream1.Flush()
stream1.Close()
'// BUILD THE ATTACHMENT BODY PART (LEVEL 1)
fileBody = mailBody.AddBodyPart(2)
fileBody.ContentMediaType = _
String.Format("application/octet-stream; name=""{0}""", _
GetFullFilename(file))
fileBody.ContentTransferEncoding = "base64"
stream2 = fileBody.GetDecodedContentStream()
stream2.Type = StreamTypeEnum.adTypeBinary
stream2.Write(fileContents)
stream2.Flush()
stream2.Close()
'// SET MESSAGE PROPERTIES AND SEND THE MESSAGE
mail.To = recipientEmailAddress
mail.From = senderEmailAddress
mail.Subject = emailSubject
mail.Send()
'// FOR TESTING PURPOSES ONLY; WRITES OUT THE TEXT CONTENTS OF THE
'// MESSAGE TO A FILE
'Dim output As ADODB.Stream = mail.GetStream()
'output.SaveToFile( _
'"C:\documents and settings\chen\desktop\output.txt", _
'SaveOptionsEnum.adSaveCreateOverWrite)
SuccessLabel.Visible = True
SuccessLabel.Text = "Your message was sent successfully."
Catch ex As Exception
'// SHOW ERROR MESSAGE
Trace.Warn("[ERROR]", _
"An error occurred while trying to attach and send the message",_
ex)
ErrorLabel.Text = "An error occurred while trying to send the email "
ErrorLabel.Text = ErrorLabel.Text & "with the attachment..."
ErrorLabel.Visible = True
Finally
stream1 = Nothing
stream2 = Nothing
End Try
Else
'// FILE IS NOT VALID; DISPLAY ERROR
ErrorLabel.Text = message
ErrorLabel.Visible = True
End If
End SubNot much to it, right :) Just a few things to take note of:
The IBodyPart MSDN documentation has a good sample of working with multi-level body parts.
Testing should be pretty obvious. Before you do so, make sure that you change the MailServer key in the config file. Please note that you may also have to add authentication to the CDO.Configration fields as well to access your mail server. For the purposes of testing, I recommend that you enable tracing and page output (if you're using my code, exceptions are written to the trace).


Most mail clients will allow you to peek at the raw SMTP message that was received. You can peek at it to check out how CDOSYS built up the mail message.
In this workshop, we've discovered how to utlize CDOSYS to access the full functionality that is is masked by System.Web.Mail to do some cool stuff. CDOSYS has a lot of cool features, including the CreateMHTMLBody() function, which I will reserve for a future workshop. Also in mind is a workshop to create an email message with an embedded background image. This would be cool for creating mail templates/stationary that could be sent without using a URL reference to download the background images (since many clients nowadays filter those HTTP requests anyways).
Additional resources for further exploration: