Sending an Email from within Microsoft Word – with the Word document attached

Recently whilst working on a project I had the requirement to send an email from within Word, initiated from the Ribbon command, that contained the Word document as an attachment. Whilst sending an SMTP message was straight forward enough the challenge became accessing the currently active document, as a Stream, without first closing the document an access a file stream.

This process was to be initiated from a Ribbon button:

image

Within Word there is a property that is made available called WordOpenXml, that provides access to the document as a XML based Flat OPC Format document.

 Word._Document document = Globals.ThisAddIn.Application.ActiveDocument;
string wordOpenXML = document.WordOpenXML;
XDocument openDocument = XDocument.Load(new StringReader(wordOpenXML));

The Flat OPC Format is discussed in detail in Eric White’s blog; along with several associated blogs on how this can be used (well worth a read). Luckily Eric White has documented how to convert this single XML document into a stream representation of the open Word document as an Open Xml Document. As this code is extremely useful I have included my version below (see Eric blog posting Transforming Flat OPC Format to Open XML Documents for more details):

 public static void FlatOpenXmlToOpc(XDocument document, Stream docxStream)
{
    XNamespace pkg = "https://schemas.microsoft.com/office/2006/xmlPackage";
    XNamespace rel = "https://schemas.openxmlformats.org/package/2006/relationships";

    using (Package package = Package.Open(docxStream, FileMode.Create))
    {
        // add all parts (but not relationships)
        foreach (var xmlPart in document.Root
            .Elements()
            .Where(p =>
                (string)p.Attribute(pkg + "contentType") != "application/vnd.openxmlformats-package.relationships+xml"))
        {
            string name = (string)xmlPart.Attribute(pkg + "name");
            string contentType = (string)xmlPart.Attribute(pkg + "contentType");
            if (contentType.EndsWith("xml", StringComparison.OrdinalIgnoreCase))
            {
                Uri u = new Uri(name, UriKind.Relative);
                PackagePart part = package.CreatePart(u, contentType, CompressionOption.SuperFast);
                using (Stream str = part.GetStream(FileMode.Create))
                using (XmlWriter xmlWriter = XmlWriter.Create(str))
                {
                    xmlPart.Element(pkg + "xmlData")
                        .Elements()
                        .First()
                        .WriteTo(xmlWriter);
                }
            }
            else
            {
                Uri u = new Uri(name, UriKind.Relative);
                PackagePart part = package.CreatePart(u, contentType, CompressionOption.SuperFast);
                using (Stream str = part.GetStream(FileMode.Create))
                using (BinaryWriter binaryWriter = new BinaryWriter(str))
                {
                    string base64StringInChunks = (string)xmlPart.Element(pkg + "binaryData");
                    char[] base64CharArray = base64StringInChunks.Where(c => c != '\r' && c != '\n').ToArray();
                    byte[] byteArray = System.Convert.FromBase64CharArray(base64CharArray, 0, base64CharArray.Length);
                    binaryWriter.Write(byteArray);
                }
            }
        }

        foreach (var xmlPart in document.Root.Elements())
        {
            string name = (string)xmlPart.Attribute(pkg + "name");
            string contentType = (string)xmlPart.Attribute(pkg + "contentType");
            if (contentType == "application/vnd.openxmlformats-package.relationships+xml")
            {
                if (name == "/_rels/.rels")
                {
                    // add the package level relationships
                    foreach (XElement xmlRel in xmlPart.Descendants(rel + "Relationship"))
                    {
                        string id = (string)xmlRel.Attribute("Id");
                        string type = (string)xmlRel.Attribute("Type");
                        string target = (string)xmlRel.Attribute("Target");
                        string targetMode = (string)xmlRel.Attribute("TargetMode");
                        if (targetMode == "External")
                        {
                            package.CreateRelationship(new Uri(target, UriKind.Absolute), TargetMode.External, type, id);
                        }
                        else
                        {
                            package.CreateRelationship(new Uri(target, UriKind.Relative), TargetMode.Internal, type, id);
                        }
                    }
                }
                else
                {
                    // add part level relationships
                    string directory = name.Substring(0, name.IndexOf("/_rels", StringComparison.OrdinalIgnoreCase));
                    string relsFilename = name.Substring(name.LastIndexOf('/'));
                    string filename = relsFilename.Substring(0, relsFilename.IndexOf(".rels", StringComparison.OrdinalIgnoreCase));
                    PackagePart fromPart = package.GetPart(new Uri(directory + filename, UriKind.Relative));
                    foreach (XElement xmlRel in xmlPart.Descendants(rel + "Relationship"))
                    {
                        string id = (string)xmlRel.Attribute("Id");
                        string type = (string)xmlRel.Attribute("Type");
                        string target = (string)xmlRel.Attribute("Target");
                        string targetMode = (string)xmlRel.Attribute("TargetMode");
                        if (targetMode == "External")
                        {
                            fromPart.CreateRelationship(new Uri(target, UriKind.Absolute), TargetMode.External, type, id);
                        }
                        else
                        {
                            fromPart.CreateRelationship(new Uri(target, UriKind.Relative), TargetMode.Internal, type, id);
                        }
                    }
                }
            }
        }
    }
}

 

 

Once I had access to the active document as a Open Xml Document stream, I was able to extract any necessary element’s from the document, including custom XML parts.

 XDocument dataDocument = null;
using (Package packageFile = Package.Open(docxStream, FileMode.Open, FileAccess.Read))
{
    foreach (PackagePart part in packageFile.GetParts())
    {
        if (part.ContentType.Equals("application/xml", StringComparison.OrdinalIgnoreCase))
        {
            using (StreamReader streamReader = new StreamReader(part.GetStream()))
            {
                XDocument packageDocument = XDocument.Load(XmlReader.Create(streamReader));

                if (packageDocument != null && packageDocument.Root.Name.NamespaceName == "MyDataNamespace")
                {
                    dataDocument = packageDocument;
                }
            }
        }
    }
}

As you can see, at this point, the Open Xml Document is available as a stream, along with the associated package elements. Thus to send an SMTP message I merely has to create the attachment and email body.

To create the email body there was the option to perform a XSLT transform, from XML to HTML, directly from the Flat OPC Format; again covered by Eric White in his blog. However for my requirements the easiest option was to transform the extracted XML document.

 using (XmlReader document = dataDocument.Root.CreateReader())
{
    // Load an XPathDocument, XslTransform, and define the transform variables
    XPathDocument xDocument = new XPathDocument(document);
    XmlNamespaceManager nsmgr = new XmlNamespaceManager(document.NameTable);
    nsmgr.AddNamespace("n1", "MyDataNamespace");
 
    // Define the writer settings
    XmlWriterSettings settings = new XmlWriterSettings();
    settings.Indent = true;
    settings.OmitXmlDeclaration = false;
    settings.NewLineHandling = NewLineHandling.Replace;
 
    // Convert the message XML into a stream for sending as an attachment and transforming
    MemoryStream dataStream = new MemoryStream();
    using (XmlWriter xmlWriter = XmlWriter.Create(cdaStream, settings))
    {
        // push the clincal document into the writer and hence memory stream
        xDocument.CreateNavigator().WriteSubtree(xmlWriter);
        xmlWriter.Flush();

        // use the memory stream to process a transform
        dataStream.Seek(0, SeekOrigin.Begin);
        StringBuilder builder = new StringBuilder();
        using (StringWriter htmlWriter = new StringWriter(builder, CultureInfo.InvariantCulture))
        {
            Stream readerTransform = Assembly.GetExecutingAssembly().GetManifestResourceStream("MyNamespace.MyTransform.xml");
            XslCompiledTransform transform = new XslCompiledTransform();
            transform.Load(XmlReader.Create(readerTransform));
            transform.Transform(XmlReader.Create(dataStream), null, htmlWriter);
        }
 
        string body = builder.ToString();
    }
}

At this point the email body had been defined enabling to to be used to create an SMTP message:

 MailMessage message = new MailMessage();
message.IsBodyHtml = true;
message.Body = body;

To define an attachment I then merely had to use the Open Xml Document stream:

 docxStream.Seek(0, SeekOrigin.Begin);
Attachment attachment = new Attachment(docxStream, "MyDocumentName.docx");
message.Attachments.Add(attachment);

I did have to remember to reset the current location of the document stream to ensure it is positioned at the start of the stream. Failure to do this will resulted in an attachment with no contents.

Hopefully I have demonstrated that managing SMTP email’s from within Word, that contains the actual Word document as an attachment, is actually a simple task. The key to making this work is the ability to access the Flat OPC Format and then convert this into an Open Xml Document.

Written by Carl Nolan