Skip to content

Commit

Permalink
Added support for decoding SMTP DATA to the SmtpDataFilter
Browse files Browse the repository at this point in the history
Fixes issue #1607
  • Loading branch information
jstedfast committed Jul 13, 2023
1 parent 7d51476 commit 3a2bfb9
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 20 deletions.
22 changes: 21 additions & 1 deletion Documentation/Examples/SmtpExamples.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public static void SaveToPickupDirectory (MimeMessage message, string pickupDire
// which means that lines beginning with "." need to be escaped
// by adding an extra "." to the beginning of the line.
//
// Use an SmtpDataFilter "byte-stuff" the message as it is written
// Use an SmtpDataFilter to "byte-stuff" the message as it is written
// to the file stream. This is the same process that an SmtpClient
// would use when sending the message in a `DATA` command.
using (var filtered = new FilteredStream (stream)) {
Expand All @@ -89,6 +89,26 @@ public static void SaveToPickupDirectory (MimeMessage message, string pickupDire
}
#endregion

#region LoadFromPickupDirectory
public static MimeMessage LoadFromPickupDirectory (string fileName)
{
using (var stream = File.OpenRead (fileName)) {
// IIS pickup directories store messages that have been "byte-stuffed"
// which means that lines beginning with "." have been escaped by
// adding an extra "." to the beginning of the line.
//
// Use an SmtpDataFilter to decode the message as it is loaded from
// the file stream. This is the reverse process that an SmtpClient
// would use when sending the message in a `DATA` command.
using (var filtered = new FilteredStream (stream)) {
filtered.Add (new SmtpDataFilter (decode: true));

return MimeMessage.Load (filtered);
}
}
}
#endregion

#region ProtocolLogger
public static void SendMessage (MimeMessage message)
{
Expand Down
74 changes: 55 additions & 19 deletions MailKit/Net/Smtp/SmtpDataFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,17 @@ namespace MailKit.Net.Smtp {
/// An SMTP filter designed to format a message stream for the DATA command.
/// </summary>
/// <remarks>
/// A special stream filter that escapes lines beginning with a '.' as needed when
/// sending a message via the SMTP protocol or when saving a message to an IIS
/// message pickup directory.
/// A special stream filter that can encode or decode lines beginning with a '.' as
/// needed when sending/receiving a message via the SMTP protocol or when saving a
/// message to an IIS message pickup directory.
/// </remarks>
/// <example>
/// <code language="c#" source="Examples\SmtpExamples.cs" region="SaveToPickupDirectory" />
/// <code language="c#" source="Examples\SmtpExamples.cs" region="LoadFromPickupDirectory" />
/// </example>
public class SmtpDataFilter : MimeFilterBase
{
readonly bool decode;
bool bol;

/// <summary>
Expand All @@ -48,29 +50,18 @@ public class SmtpDataFilter : MimeFilterBase
/// <remarks>
/// Creates a new <see cref="SmtpDataFilter"/>.
/// </remarks>
/// <param name="decode"><c>true</c> if the filter should decode the content; otherwise, <c>false</c>.</param>
/// <example>
/// <code language="c#" source="Examples\SmtpExamples.cs" region="SaveToPickupDirectory" />
/// <code language="c#" source="Examples\SmtpExamples.cs" region="LoadFromPickupDirectory" />
/// </example>
public SmtpDataFilter ()
public SmtpDataFilter (bool decode = false)
{
this.decode = decode;
bol = true;
}

/// <summary>
/// Filter the specified input.
/// </summary>
/// <remarks>
/// Filters the specified input buffer starting at the given index,
/// spanning across the specified number of bytes.
/// </remarks>
/// <returns>The filtered output.</returns>
/// <param name="input">The input buffer.</param>
/// <param name="startIndex">The starting index of the input buffer.</param>
/// <param name="length">The length of the input buffer, starting at <paramref name="startIndex"/>.</param>
/// <param name="outputIndex">The output index.</param>
/// <param name="outputLength">The output length.</param>
/// <param name="flush">If set to <c>true</c>, all internally buffered data should be flushed to the output buffer.</param>
protected override byte[] Filter (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength, bool flush)
byte[] Encode (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength, bool flush)
{
int inputEnd = startIndex + length;
bool escape = bol;
Expand Down Expand Up @@ -125,6 +116,51 @@ protected override byte[] Filter (byte[] input, int startIndex, int length, out
return OutputBuffer;
}

byte[] Decode (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength, bool flush)
{
int inputEnd = startIndex + length;
int index = startIndex;

EnsureOutputSize (length, false);
outputLength = 0;
outputIndex = 0;

while (index < inputEnd) {
byte c = input[index++];

if (bol && c == (byte) '.') {
bol = false;
} else {
OutputBuffer[outputLength++] = c;
bol = c == (byte) '\n';
}
}

return OutputBuffer;
}

/// <summary>
/// Filter the specified input.
/// </summary>
/// <remarks>
/// Filters the specified input buffer starting at the given index,
/// spanning across the specified number of bytes.
/// </remarks>
/// <returns>The filtered output.</returns>
/// <param name="input">The input buffer.</param>
/// <param name="startIndex">The starting index of the input buffer.</param>
/// <param name="length">The length of the input buffer, starting at <paramref name="startIndex"/>.</param>
/// <param name="outputIndex">The output index.</param>
/// <param name="outputLength">The output length.</param>
/// <param name="flush">If set to <c>true</c>, all internally buffered data should be flushed to the output buffer.</param>
protected override byte[] Filter (byte[] input, int startIndex, int length, out int outputIndex, out int outputLength, bool flush)
{
if (decode)
return Decode (input, startIndex, length, out outputIndex, out outputLength, flush);

return Encode (input, startIndex, length, out outputIndex, out outputLength, flush);
}

/// <summary>
/// Reset the filter.
/// </summary>
Expand Down
32 changes: 32 additions & 0 deletions UnitTests/Net/Smtp/SmtpDataFilterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,38 @@ public class SmtpDataFilterTests
const string ComplexDataInput = "This is a bit more complicated\r\n... This line starts with a '.' and\r\ntherefore needs to be byte-stuffed\r\n. And so does this line!\r\n";
const string ComplexDataOutput = "This is a bit more complicated\r\n.... This line starts with a '.' and\r\ntherefore needs to be byte-stuffed\r\n.. And so does this line!\r\n";

[Test]
public void TestSmtpDataFilterDecode ()
{
var inputs = new string[] { SimpleDataInput, ComplexDataOutput };
var outputs = new string[] { SimpleDataInput, ComplexDataInput };
var filter = new SmtpDataFilter (decode: true);

for (int i = 0; i < inputs.Length; i++) {
using (var memory = new MemoryStream ()) {
byte[] buffer;
int n;

using (var filtered = new FilteredStream (memory)) {
filtered.Add (filter);

buffer = Encoding.ASCII.GetBytes (inputs[i]);
filtered.Write (buffer, 0, buffer.Length);
filtered.Flush ();
}

buffer = memory.GetBuffer ();
n = (int) memory.Length;

var text = Encoding.ASCII.GetString (buffer, 0, n);

Assert.AreEqual (outputs[i], text);

filter.Reset ();
}
}
}

[Test]
public void TestSmtpDataFilter ()
{
Expand Down

0 comments on commit 3a2bfb9

Please sign in to comment.