beter
95a0a5426429105764aa0bd1f7403f54ca803077
4 files changed
EmlFastDecoder/FastHelper.csEmlFastDecoder/MimePart.csEmlFastDecoder/Program.csEmlFastDecoder/RegexHelper.cs
diff --git a/EmlFastDecoder/FastHelper.cs b/EmlFastDecoder/FastHelper.cs
deleted file mode 100644
index 505e8de..0000000
--- a/EmlFastDecoder/FastHelper.cs
+++ /dev/null
@@ -1,191 +0,0 @@
-using System.Text;
-using System.Text.RegularExpressions;
-
-namespace EmlFastDecoder;
-
-public class FastHelper
-{
- private static string[] lines = [];
-
- private static readonly Regex IsValue = new(@"^\W");
-
- private static readonly Regex Boundary = new(@"\Wboundary=""([^""]*)""");
- private static readonly Regex RegCharSet = new(@"\Wcharset=""([^""]*)""");
-
- public class MimePart
- {
- public int Start { get; set; } = -1;
- public int Stop { get; set; } = -1;
- public int StartContent { get; set; } = -1;
-
- private List<(string Name, string Value)>? headers = null;
- public List<(string Name, string Value)> Headers
- {
- get
- {
- if (headers == null)
- {
- headers = [];
- for (int i = Start; i < Stop; i++)
- {
- var line = lines[i];
- if (line == string.Empty)
- {
- this.StartContent = i + 1;
- return headers;
- }
- var s = line.IndexOf(':');
- if (s > 0)
- {
- var name = line[..s];
- var value = new StringBuilder();
- value.Append(line[(s + 1)..].Trim());
- while (i < Stop)
- {
- if (!IsValue.Match(lines[i + 1]).Success)
- break;
- i++;
- value.Append($"{lines[i]}");
- }
- headers.Add((name, value.ToString()));
- }
- }
- }
- return headers;
- }
- }
-
- public string CharSet
- {
- get
- {
- var match = RegCharSet.Match(this["Content-Type"]);
- if (match.Success)
- return match.Groups[1].Value;
- else
- return "utf8";
- }
- }
-
- public string TextContent
- {
- get
- {
- var charset = CharSet;
- var encoding = this["Content-Transfer-Encoding"];
-
- var val = string.Join(Environment.NewLine, lines.Skip(StartContent).Take(Stop - StartContent).ToArray());
-
- if(encoding == "quoted-printable")
- val = QuotedPrintableDecoder.Decode(val, charset);
-
- return val;
- }
- }
-
- public byte[] BinContent
- {
- get
- {
- var val = string.Join("", lines.Skip(StartContent).Take(Stop - StartContent).ToArray());
-
- var encoding = this["Content-Transfer-Encoding"];
- if (encoding == "base64")
- return Convert.FromBase64String(val);
- else
- return Encoding.UTF8.GetBytes(val);
- }
- }
-
- public string this[string name] => this.Headers.FirstOrDefault(x => x.Name == name).Value ?? string.Empty;
-
-
- private List<MimePart>? parts = null;
- public List<MimePart> Parts
- {
- get
- {
- if (parts == null)
- {
- parts = [];
- var contentType = this["Content-Type"];
- var hasAttachment = this["Content-Disposition"].Contains("attachment");
- if (contentType == string.Empty)
- return parts;
-
- if (hasAttachment)
- {
- var attachment = new MimePart()
- {
- Start = StartContent,
- Stop = Stop,
- StartContent = StartContent
- };
- parts.Add(attachment);
- return parts;
- }
-
- if (!contentType.StartsWith("multipart/"))
- return parts;
-
- var match = Boundary.Match(contentType);
- if (!match.Success)
- return parts;
- var boundaryId = match.Groups[1].Value;
- var boundary = $"--{boundaryId}";
- var count = 0;
- MimePart? part = null;
- for (int i = Start; i < Stop; i++)
- {
- var line = lines[i];
- if (line.StartsWith(boundary))
- {
- if (part != null)
- part.Stop = i - 1;
- if (line.StartsWith($"{boundary}--"))
- break;
- part = new MimePart()
- {
- Start = i + 1
- };
- parts.Add(part);
- count++;
- //Debug.WriteLine($"{count} {i + 1} {line}");
- }
- }
- }
- return parts;
- }
- }
-
- public async Task SaveAsync(string Path) => await SaveAsync(Path, DateTime.Now);
- public async Task SaveAsync(string Path, DateTime dtm)
- {
- await File.WriteAllLinesAsync(Path, lines.Skip(StartContent).Take(Stop - StartContent).ToArray());
- File.SetAttributes(Path, FileAttributes.ReadOnly);
- File.SetLastWriteTimeUtc(Path, dtm);
- File.SetCreationTime(Path, dtm);
- }
- }
-
- public static void Clear()
- {
- lines = [];
- }
-
- public static async Task<MimePart> ReadEmlAsync(string path)
- {
- Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
-
- lines = await File.ReadAllLinesAsync(path);
-
- MimePart emailMessage = new()
- {
- Start = 0,
- Stop = lines.Length
- };
-
- return emailMessage;
- }
-
-}
diff --git a/EmlFastDecoder/MimePart.cs b/EmlFastDecoder/MimePart.cs
new file mode 100644
index 0000000..facb70f
--- /dev/null
+++ b/EmlFastDecoder/MimePart.cs
@@ -0,0 +1,184 @@
+using System.Text;
+
+namespace EmlFastDecoder;
+public class MimePart(string[] lines)
+{
+ private const int InvalidIndex = -1;
+
+ private readonly string[] _lines = lines;
+ private int _start = InvalidIndex;
+ private int _stop = InvalidIndex;
+ private int _startContent = InvalidIndex;
+
+ private IReadOnlyList<(string Name, string Value)>? headers;
+ public IReadOnlyList<(string Name, string Value)> Headers => headers ??= BuildHeaders();
+
+ private IReadOnlyList<(string Name, string Value)> BuildHeaders()
+ {
+ var result = new List<(string Name, string Value)>();
+ for (int i = _start; i < _stop; i++)
+ {
+ var line = _lines[i];
+ if (string.IsNullOrEmpty(line))
+ {
+ _startContent = i + 1;
+ return result;
+ }
+ var s = line.IndexOf(':');
+ if (s > 0)
+ {
+ var name = line[..s];
+ var value = new StringBuilder(line[(s + 1)..].Trim());
+ // NB: Laatste regel wordt genegeerd als deze exact op 'stop' valt, wat bij MIME-headers zeldzaam is.
+ while (i < _stop - 1 && RegexHelper.IsValue.IsMatch(_lines[i + 1]))
+ {
+ value.Append(_lines[++i]);
+ }
+ result.Add((name, value.ToString()));
+ }
+ }
+ return result;
+ }
+
+ private void EnsureBoundariesInitialized()
+ {
+ if (_startContent == InvalidIndex || _stop == InvalidIndex)
+ throw new InvalidOperationException("Content boundaries not initialized");
+ }
+
+ // Retourneert de charset uit Content-Type, of Encoding.UTF8.WebName ("utf-8") als deze niet gespecificeerd is.
+ public string CharSet => RegexHelper.CharSet.Match(this["Content-Type"])?.Groups[1].Value ?? Encoding.UTF8.WebName;
+
+ // Retourneert de tekstuele inhoud van deze MIME-part, gedecodeerd volgens de Content-Transfer-Encoding.
+ public string TextContent
+ {
+ get
+ {
+ EnsureBoundariesInitialized();
+
+ var val = string.Join(Environment.NewLine, _lines[_startContent.._stop]);
+
+ return this["Content-Transfer-Encoding"] switch
+ {
+ "7bit" => val, // Pure ASCII, geen decoding nodig
+ "8bit" => val, // 8-bit tekst, vertrouw op CharSet
+ "quoted-printable" => QuotedPrintableDecoder.Decode(val, CharSet),
+ "base64" => throw new InvalidOperationException("Base64 content cannot be represented as text"),
+ "binary" => throw new InvalidOperationException("Binary content cannot be represented as text"),
+ "" => val, // Geen encoding gespecificeerd, assumeer plain text
+ var enc => throw new NotSupportedException($"Unsupported Content-Transfer-Encoding: {enc}")
+ };
+ }
+ }
+
+ // Retourneert de binaire inhoud van deze MIME-part, gedecodeerd volgens de Content-Transfer-Encoding.
+ public byte[] BinaryContent
+ {
+ get
+ {
+ EnsureBoundariesInitialized();
+
+ var val = string.Join("", _lines[_startContent.._stop]);
+ return this["Content-Transfer-Encoding"] switch
+ {
+ "7bit" => Encoding.ASCII.GetBytes(val), // Alleen ASCII, veilig als bytes
+ "8bit" => Encoding.GetEncoding(CharSet).GetBytes(val), // Gebruik de gespecificeerde charset
+ "binary" => Encoding.GetEncoding(CharSet).GetBytes(val), // NB: Binary data kan corrupt raken; overweeg byte[] input voor echte support
+ "quoted-printable" => Encoding.GetEncoding(CharSet).GetBytes(QuotedPrintableDecoder.Decode(val, CharSet)),
+ "base64" => Convert.FromBase64String(val),
+ "" => Encoding.UTF8.GetBytes(val), // Geen encoding, default UTF-8
+ var enc => throw new NotSupportedException($"Unsupported Content-Transfer-Encoding: {enc}")
+ };
+ }
+ }
+
+ // Geeft de waarde van een header met de opgegeven naam, of een lege string als deze niet bestaat.
+ public string this[string name] => Headers.FirstOrDefault(x => x.Name == name).Value ?? string.Empty;
+
+ private IReadOnlyList<MimePart>? parts;
+
+ // Retourneert een read-only lijst van sub-parts (attachments of multipart secties).
+ public IReadOnlyList<MimePart> Parts => parts ??= BuildParts();
+
+ private IReadOnlyList<MimePart> BuildParts()
+ {
+ var contentType = this["Content-Type"];
+
+ if (string.IsNullOrEmpty(contentType))
+ return [];
+
+ var disposition = this["Content-Disposition"];
+ if (!string.IsNullOrEmpty(disposition) && disposition.Split(';')[0].Trim().Equals("attachment", StringComparison.OrdinalIgnoreCase))
+ {
+ return
+ [
+ new(_lines)
+ {
+ _start = _startContent,
+ _stop = _stop,
+ _startContent = _startContent
+ }
+ ];
+ }
+
+ if (!contentType.StartsWith("multipart/"))
+ return [];
+
+ var boundaryMatch = RegexHelper.Boundary.Match(contentType);
+ if (!boundaryMatch.Success)
+ return [];
+
+ var boundary = $"--{boundaryMatch.Groups[1].Value}";
+ return ParseMultipart(boundary);
+ }
+
+ // Parseert multipart MIME-content en retourneert een lijst van sub-parts gescheiden door de boundary.
+ private IReadOnlyList<MimePart> ParseMultipart(string boundary)
+ {
+ var result = new List<MimePart>();
+ MimePart? currentPart = null;
+
+ for (int i = _start; i < _stop; i++)
+ {
+ var line = _lines[i];
+ if (line.StartsWith($"{boundary}--"))
+ {
+ if (currentPart != null) // Optioneel, maar kan weg
+ currentPart._stop = i - 1;
+ break;
+ }
+
+ if (line.StartsWith(boundary))
+ {
+ if (currentPart is not null)
+ currentPart._stop = i - 1;
+ currentPart = new MimePart(_lines) { _start = i + 1 };
+ result.Add(currentPart);
+ }
+ }
+ if (currentPart is not null && currentPart._stop == InvalidIndex)
+ currentPart._stop = _stop - 1;
+ return result.AsReadOnly();
+ }
+
+ // Slaat deze MIME-part asynchroon op naar een bestand met de huidige datum/tijd.
+ public Task SaveAsync(string path) => SaveAsync(path, DateTime.Now);
+
+ // Slaat deze MIME-part asynchroon op naar een bestand met de opgegeven datum/tijd.
+ public async Task SaveAsync(string path, DateTime dtm)
+ {
+ EnsureBoundariesInitialized();
+ await File.WriteAllLinesAsync(path, _lines[_startContent.._stop]);
+ File.SetAttributes(path, FileAttributes.ReadOnly);
+ File.SetLastWriteTimeUtc(path, dtm);
+ File.SetCreationTime(path, dtm);
+ }
+
+ // Leest een .eml-bestand asynchroon en retourneert een MimePart met de geparste inhoud.
+ public static async Task<MimePart> ReadEmlAsync(string path)
+ {
+ Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
+ var lines = await File.ReadAllLinesAsync(path);
+ return new MimePart(lines) { _start = 0, _stop = lines.Length };
+ }
+}
\ No newline at end of file
diff --git a/EmlFastDecoder/Program.cs b/EmlFastDecoder/Program.cs
index dac39ee..0281464 100644
--- a/EmlFastDecoder/Program.cs
+++ b/EmlFastDecoder/Program.cs
@@ -1,14 +1,16 @@
using EmlFastDecoder;
using System.Diagnostics;
-static async Task ShowPartsAsync(int Depth, FastHelper.MimePart part)
+static async Task ShowPartsAsync(int Depth, MimePart part)
{
var contentType = part["Content-Type"];
- for (var i = 0; i < Depth; i++)
- Debug.Write('\t');
if (contentType != string.Empty)
+ {
+ for (var i = 0; i < Depth; i++)
+ Debug.Write('\t');
Debug.WriteLine(contentType);
+ }
if (part["Content-Disposition"].Contains("attachment"))
{
@@ -37,13 +39,12 @@ static async Task ShowPartsAsync(int Depth, FastHelper.MimePart part)
{
await ShowPartsAsync(Depth, subpart);
}
- Depth--;
}
var sw = Stopwatch.StartNew();
//var email = await FastHelper.ReadEmlAsync(@"input\20240603-135130.eml");
-var email = await FastHelper.ReadEmlAsync(@"input\jun-jul 2024.eml");
+var email = await MimePart.ReadEmlAsync(@"input\jun-jul 2024.eml");
await ShowPartsAsync(0, email);
diff --git a/EmlFastDecoder/RegexHelper.cs b/EmlFastDecoder/RegexHelper.cs
new file mode 100644
index 0000000..0297a96
--- /dev/null
+++ b/EmlFastDecoder/RegexHelper.cs
@@ -0,0 +1,19 @@
+using System.Text.RegularExpressions;
+
+namespace EmlFastDecoder;
+
+public partial class RegexHelper
+{
+ public static readonly Regex IsValue = IsValueRegex();
+ public static readonly Regex Boundary = BoundaryRegex();
+ public static readonly Regex CharSet = CharsetRegex();
+
+ [GeneratedRegex(@"^\W")]
+ private static partial Regex IsValueRegex();
+
+ [GeneratedRegex(@"\Wboundary=""([^""]*)""")]
+ private static partial Regex BoundaryRegex();
+
+ [GeneratedRegex(@"\Wcharset=""([^""]*)""")]
+ private static partial Regex CharsetRegex();
+}