I'm creating an email with a MIME attachment from a BizTalk 2016 SMTP Send Port.
However, I think any knowledge that anyone can share from any other language about the oddities of Outlook and MIME might help me fix the issue below.
In Outlook, the attachment shows as body.txt, but when I click "File Save" it shows the name that I used when I created it (and that's what the user wants to see).
What I'm referring to is the the left side where it says "body.txt" above the 5k and to the right of the attachment icon in the screen shot below:
In BizTalk C# Pipeline component, that attachment was set with the following code, where we are setting Context properties on the BizTalk Message.
I also tried setting ContentHeader and ContentID.
strFilename = "MyFileName_693.txt"; // Just for example.
pInMsg.BodyPart.PartProperties.Write(
"FileName",
"http://schemas.microsoft.com/BizTalk/2003/mime-properties",
strFilename);
When I forwarded the email to my Gmail, the attachment was shown with the proper name. So my question is particular to making it appear with the desired name in Outlook (2016).
So far I've got this working with an orchestration with a dynamic send port. It's still a bit of work, but it gets the job done with the stock component. Following description is based on the stock SMTP-adapter included in BizTalk 2013R2.
Note: even though my solution works, it feels like a workaround and something i shouldn't have to do, if the adapter was just slightly smarter about this.
First of all, let's look example email snippet which causes issues in some clients:
------=_NextPart_000_0001_01D4502F.8A6A1500
Content-Transfer-Encoding: 7bit
Content-Type: text/plain; charset="utf-8"
See attached email.
------=_NextPart_000_0001_01D4502F.8A6A1500
Content-Type: application/pdf; name="CDM_Order - Copy.pdf"
Content-Disposition: attachment; filename="CDM_Order - Copy.pdf"
Content-Description: body
Content-Transfer-Encoding: base64
JVBERi0xLjQKJeLjz9MNCjUgMCBvYmoKPDwvRFsgMyAwIFIvWFlaIG51bGwgODQxLjg4OTc3IG51
bGwgXQo+PgplbmRvYmoKOCAwIG9iago8PC9EWyAzIDAgUi9YWVogbnVsbCAyOTAuMjM2NTcgbnVs
bCBdCj4+ (etc etc base64 your file)...
Notice the Content-Description: body
part. This is the reason why some clients read body.xml
or in my case body.pdf
, even though the Disposition part looks great: Content-Disposition: attachment; filename="CDM_Order - Copy.pdf"
.
Hard setting MIME.FileName
isn't just going to work, even though it will set the Content-Disposition
right eventually, it'll never update the Content-Description
. This is because either on a static send port you've set the Attach only body part
or you specified the corresponding numeric value 1
on a dynamic send port.
However, it will work with the Attach all parts
or 2
value for the type MessagePartsAttachments
. This involves making a multi-part message in your orchestration. This will have two parts;
- First one is the
BodyPart
, now this one will include your message text and not your attachment. Make sure you specify this one as Message Body Part
in the Message Type
.
- Second part will be your actual attachment, specify this type according to your attachment type. I named this
Attachment
in this example.
Now you might think it will send the BodyPart
as attachment as well since i've said we needed Attach all parts
. This is true, so to correct that, your BodyPart
has to be defined as a RawString
, this turns the string into plain text in the BizTalk message part. For completeness i'll put the C# class at the bottom for reference.
Now that it's defined as a RawString
, the SMTP adapter will put this as the body instead of as attachment. As a side effect, the SMTP adapter will no longer put the Content-Description: body
part in the attachment part, but in the actual body part instead. It looks like this:
------=_NextPart_000_0001_01D450E4.A7E9A5E0
Content-Transfer-Encoding: 7bit
Content-Type: text/plain; charset="utf-8"
Content-Description: body
See attached email.
------=_NextPart_000_0001_01D450E4.A7E9A5E0
Content-Type: application/pdf; name="ID_0_Nummer_0.pdf"
Content-Disposition: attachment; filename="ID_0_Nummer_0.pdf"
Content-Transfer-Encoding: base64
JVBERi0xLjQKJeLjz9MNCjUgMCBvYmoKPDwvRFsgMyAwIFIvWFlaIG51bGwgODQxLjg4OTc3IG51
bGwgXQo+PgplbmRvYmoKOCAwIG9iago8PC9EWyAzIDAgUi9YWVogbnVsbCAyOTAuMjM2NTcgbnVs
bCBdCj4+ (etc etc base64 your file)...
Really nothing else is different except the placement of the Content-Description: body
part, exactly what we want. Now the email looks fine for every client.
The most important properties, besides the ones i already mentioned, must be set as well to make it behave properly:
Content type of your body:
MsgPdfOrder.BodyPart(Microsoft.XLANGs.BaseTypes.ContentType) = "text/plain";
Content type of your attachment:
MsgPdfOrder.Attachment(Microsoft.XLANGs.BaseTypes.ContentType) = "application/pdf";
Attachment filename:
MsgPdfOrder.Attachment(MIME.FileName) = "CDM_Order - Copy.pdf"
Body character set (will result in Unknown Error Description
if not set):
MsgPdfOrder(SMTP.EmailBodyTextCharset) = "UTF-8";
Make sure you don't set the SMTP.EmailBodyText
because we already have the BodyPart
for that.
RawString class, use it like this in an orchestration MsgPdfOrder.BodyPart = new Yournamespace.Components.RawString("See attached email.");
:
using System.Runtime.Serialization;
using System;
using System.IO;
using System.Text;
using System.Xml.Serialization;
using Microsoft.XLANGs.BaseTypes;
namespace Yournamespace.Components
{
public abstract class BaseFormatter : IFormatter
{
public virtual SerializationBinder Binder
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}
public virtual StreamingContext Context
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}
public virtual ISurrogateSelector SurrogateSelector
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}
public abstract void Serialize(Stream stm, object obj);
public abstract object Deserialize(Stream stm);
}
public class RawStringFormatter : BaseFormatter
{
public override void Serialize(Stream s, object o)
{
RawString rs = (RawString)o;
byte[] ba = rs.ToByteArray();
s.Write(ba, 0, ba.Length);
}
public override object Deserialize(Stream stm)
{
StreamReader sr = new StreamReader(stm, true);
string s = sr.ReadToEnd();
return new RawString(s);
}
}
[CustomFormatter(typeof(RawStringFormatter))]
[Serializable]
public class RawString
{
[XmlIgnore]
string _val;
public RawString(string s)
{
if (null == s)
throw new ArgumentNullException();
_val = s;
}
public RawString()
{
}
public byte[] ToByteArray()
{
return Encoding.UTF8.GetBytes(_val);
}
public override string ToString()
{
return _val;
}
}
}