-
Notifications
You must be signed in to change notification settings - Fork 2
feat: add native POP3 receiver adapters (MailKit + Rebex) #187
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,78 @@ | ||||||||||||||||||||||
| using System.Text; | ||||||||||||||||||||||
| using MailKit.Net.Pop3; | ||||||||||||||||||||||
| using MailKit.Security; | ||||||||||||||||||||||
| using MimeKit; | ||||||||||||||||||||||
| using SW.PrimitiveTypes; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| namespace SW.Bitween.NativeAdapters.Pop3Receiver; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public class NativePop3Receiver : INativeInfolinkReceiver | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| private Pop3ReceiverInput _options = new(); | ||||||||||||||||||||||
| private Pop3Client? _client; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Connection defaults matching the standard implicit-SSL POP3 endpoint. | ||||||||||||||||||||||
| // Not user-configurable; exposed internally only so tests can point at a local fake server. | ||||||||||||||||||||||
| internal int Port { get; set; } = 995; | ||||||||||||||||||||||
| internal bool UseSsl { get; set; } = true; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public async Task Initialize() | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| _client = new Pop3Client(); | ||||||||||||||||||||||
| var sslOptions = UseSsl ? SecureSocketOptions.SslOnConnect : SecureSocketOptions.None; | ||||||||||||||||||||||
| await _client.ConnectAsync(_options.Host, Port, sslOptions); | ||||||||||||||||||||||
| await _client.AuthenticateAsync(_options.Username, _options.Password); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public async Task Finalize() | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| await _client!.DisconnectAsync(true); | ||||||||||||||||||||||
| _client.Dispose(); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public Task<IEnumerable<string>> ListFiles() | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| var count = Math.Min(_client!.Count, _options.BatchSize); | ||||||||||||||||||||||
| return Task.FromResult(Enumerable.Range(0, count).Select(i => i.ToString())); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Comment on lines
+33
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Negative
- var count = Math.Min(_client!.Count, _options.BatchSize);
+ var count = Math.Max(0, Math.Min(_client!.Count, _options.BatchSize));📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public async Task<XchangeFile> GetFile(string fileId) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| var index = int.Parse(fileId); | ||||||||||||||||||||||
| var message = await _client!.GetMessageAsync(index); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| var attachment = message.Attachments.FirstOrDefault(); | ||||||||||||||||||||||
| if (attachment == null) | ||||||||||||||||||||||
| return new XchangeFile(message.TextBody ?? message.HtmlBody ?? string.Empty, message.Subject); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| using var memoryStream = new MemoryStream(); | ||||||||||||||||||||||
| if (attachment is MessagePart rfc822) | ||||||||||||||||||||||
| await rfc822.Message.WriteToAsync(memoryStream); | ||||||||||||||||||||||
| else | ||||||||||||||||||||||
| await ((MimePart)attachment).Content.DecodeToAsync(memoryStream); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| var buffer = memoryStream.ToArray(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return _options.ResponseEncoding.ToLower() switch | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| "base64" => new XchangeFile(Convert.ToBase64String(buffer), message.Subject), | ||||||||||||||||||||||
| "utf8" => new XchangeFile(Encoding.UTF8.GetString(buffer), message.Subject), | ||||||||||||||||||||||
| _ => throw new ArgumentException( | ||||||||||||||||||||||
| $"Unknown {nameof(Pop3ReceiverInput.ResponseEncoding)} '{_options.ResponseEncoding}'") | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public async Task DeleteFile(string fileId) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| await _client!.DeleteMessageAsync(int.Parse(fileId)); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Comment on lines
+19
to
+68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🔵 Trivial No timeout/cancellation on any network call.
🤖 Prompt for AI Agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public string Name => "NativePop3Receiver"; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public void InitializeStartupValues(IDictionary<string, string> settings) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| _options = settings.ConvertTo<Pop3ReceiverInput>(); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public Type StartupValuesType => typeof(Pop3ReceiverInput); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,23 @@ | ||||||||||||
| using System.ComponentModel; | ||||||||||||
| using System.ComponentModel.DataAnnotations; | ||||||||||||
|
|
||||||||||||
| namespace SW.Bitween.NativeAdapters.Pop3Receiver; | ||||||||||||
|
|
||||||||||||
| public class Pop3ReceiverInput | ||||||||||||
| { | ||||||||||||
| [Required] | ||||||||||||
| public string Host { get; set; } = string.Empty; | ||||||||||||
|
|
||||||||||||
| [Required] | ||||||||||||
| public string Username { get; set; } = string.Empty; | ||||||||||||
|
|
||||||||||||
| [Required] | ||||||||||||
| [Secure] | ||||||||||||
| public string Password { get; set; } = string.Empty; | ||||||||||||
|
|
||||||||||||
| [DefaultValue(50)] | ||||||||||||
| public int BatchSize { get; set; } = 50; | ||||||||||||
|
Comment on lines
+18
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win No bound validation on Negative values pass through unvalidated and cause Proposed fix+ [Range(1, int.MaxValue)]
[DefaultValue(50)]
public int BatchSize { get; set; } = 50;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||
|
|
||||||||||||
| [DefaultValue("utf8")] | ||||||||||||
| public string ResponseEncoding { get; set; } = "utf8"; | ||||||||||||
| } | ||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,81 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using System.Text; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using Rebex.Net; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using SW.PrimitiveTypes; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| namespace SW.Bitween.NativeAdapters.RebexPop3Receiver; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public class NativeRebexPop3Receiver : INativeInfolinkReceiver | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public const string LicenseKeyEnvironmentVariable = "REBEX_LICENSE_KEY"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private RebexPop3ReceiverInput _options = new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private Pop3 _pop3 = new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Connection defaults matching the standard implicit-SSL POP3 endpoint. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Not user-configurable; exposed internally only so tests can point at a local fake server. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| internal int Port { get; set; } = 995; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| internal bool UseSsl { get; set; } = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public async Task Initialize() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Rebex.Licensing.Key = Environment.GetEnvironmentVariable(LicenseKeyEnvironmentVariable); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _pop3 = new Pop3(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var sslMode = UseSsl ? SslMode.Implicit : SslMode.None; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await _pop3.ConnectAsync(_options.Host, Port, sslMode); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await _pop3.LoginAsync(_options.Username, _options.Password); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+19
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win Same partial-init leak risk as If 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public async Task Finalize() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await _pop3.DisconnectAsync(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _pop3.Dispose(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public async Task<IEnumerable<string>> ListFiles() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var messages = await _pop3.GetMessageListAsync(Pop3ListFields.Fast); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return messages.Select(m => m.SequenceNumber.ToString()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .Take(_options.BatchSize) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .ToList(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public async Task<XchangeFile> GetFile(string fileId) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var sequenceNumber = int.Parse(fileId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var message = await _pop3.GetMailMessageAsync(sequenceNumber); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (message.Attachments.Count < 1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return new XchangeFile(message.BodyText, message.Subject); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var attachment = message.Attachments[0]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await _pop3.GetMessageAsync(sequenceNumber, attachment.FileName); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await using var stream = attachment.GetContentStream(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using var memoryStream = new MemoryStream(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await stream.CopyToAsync(memoryStream); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var buffer = memoryStream.ToArray(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+43
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Attempt to confirm Rebex.Net.Pop3.GetMessage(int, string) semantics via public docs.
curl -s -A "Mozilla/5.0" "https://www.rebex.net/support/api/mail/Rebex.Net~Rebex.Net.Pop3~GetMessage.html" | grep -i -A5 "GetMessage" | head -80
curl -s -A "Mozilla/5.0" "https://www.rebex.net/support/api/mail/Rebex.Net~Rebex.Net.Pop3~Disconnect.html" | grep -i -A5 "Disconnect" | head -80Repository: simplify9/Bitween-api Length of output: 835 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Locate Rebex references and package/version info.
git ls-files | rg -n '(^|/)SW\.Bitween\.NativeAdapters|Rebex|packages\.lock|\.csproj$|Directory\.Packages\.props$|Directory\.Build\.props$' -N
printf '\n--- Rebex references ---\n'
rg -n --hidden --glob '!**/bin/**' --glob '!**/obj/**' 'Rebex|Pop3|GetMessageAsync|DisconnectAsync|GetMailMessageAsync' .
printf '\n--- project files ---\n'
fd -a -t f '.*\.csproj$|packages\.lock$|Directory\.Packages\.props$|Directory\.Build\.props$' .
printf '\n--- NativeRebexPop3Receiver.cs around the relevant lines ---\n'
sed -n '1,120p' SW.Bitween.NativeAdapters/RebexPop3Receiver/NativeRebexPop3Receiver.csRepository: simplify9/Bitween-api Length of output: 15532 🌐 Web query:
💡 Result: In the Rebex POP3 component, the GetMessage and GetMessageAsync methods are designed to download full message data (raw MIME) to a local file or stream, rather than individual attachments [1][2][3]. The overload GetMessage(int sequenceNumber, string fileName) (and its asynchronous equivalent GetMessageAsync) accepts a string parameter representing the full local file path where the message data will be saved [1][2]. This parameter must be a valid file path and cannot be a directory [1][2]. To handle attachments, you should not use GetMessage with a file path; instead, you must first retrieve the message object using GetMailMessage(int sequenceNumber) [3]. This returns a MailMessage object, which provides access to an Attachments collection [4][5]. You can then iterate through these attachments and save them individually using the Attachment.Save(string fileName) method [6][5]. It is recommended to provide a full path (e.g., using System.IO.Path.Combine) when saving attachments to ensure they are stored in the desired directory [6]. Summary of Methods: - GetMessage(int, string): Downloads the raw entire message to a local file [2]. - GetMailMessage(int): Returns a MailMessage object containing headers, body, and attachments [3]. - Attachment.Save(string): Saves an individual attachment to a specific local file path [6][5]. Citations:
Remove the raw-message download here Proposed fix- await _pop3.GetMessageAsync(sequenceNumber, attachment.FileName);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return _options.ResponseEncoding.ToLower() switch | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "base64" => new XchangeFile(Convert.ToBase64String(buffer), message.Subject), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "utf8" => new XchangeFile(Encoding.UTF8.GetString(buffer), message.Subject), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _ => throw new ArgumentException( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $"Unknown {nameof(RebexPop3ReceiverInput.ResponseEncoding)} '{_options.ResponseEncoding}'") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public async Task DeleteFile(string fileId) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await _pop3.DeleteAsync(int.Parse(fileId)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public string Name => "NativeRebexPop3Receiver"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void InitializeStartupValues(IDictionary<string, string> settings) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _options = settings.ConvertTo<RebexPop3ReceiverInput>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public Type StartupValuesType => typeof(RebexPop3ReceiverInput); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| using System.ComponentModel; | ||
| using System.ComponentModel.DataAnnotations; | ||
|
|
||
| namespace SW.Bitween.NativeAdapters.RebexPop3Receiver; | ||
|
|
||
| public class RebexPop3ReceiverInput | ||
| { | ||
| [Required] | ||
| public string Host { get; set; } = string.Empty; | ||
|
|
||
| [Required] | ||
| public string Username { get; set; } = string.Empty; | ||
|
|
||
| [Required] | ||
| [Secure] | ||
| public string Password { get; set; } = string.Empty; | ||
|
|
||
| [DefaultValue(50)] | ||
| public int BatchSize { get; set; } = 50; | ||
|
|
||
| [DefaultValue("utf8")] | ||
| public string ResponseEncoding { get; set; } = "utf8"; | ||
| } | ||
|
Comment on lines
+6
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value Duplicate DTO shape with Identical to 🤖 Prompt for AI Agents |
||
Uh oh!
There was an error while loading. Please reload this page.