Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions SW.Bitween.NativeAdapters/Pop3Receiver/NativePop3Receiver.cs
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);
}
Comment thread
hamzahalq marked this conversation as resolved.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Negative BatchSize crashes ListFiles.

Math.Min(_client!.Count, _options.BatchSize) can be negative, and Enumerable.Range(0, count) throws ArgumentOutOfRangeException for a negative count (contrast with Rebex's .Take(), which tolerates negatives). Clamp defensively:

-        var count = Math.Min(_client!.Count, _options.BatchSize);
+        var count = Math.Max(0, Math.Min(_client!.Count, _options.BatchSize));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public Task<IEnumerable<string>> ListFiles()
{
var count = Math.Min(_client!.Count, _options.BatchSize);
return Task.FromResult(Enumerable.Range(0, count).Select(i => i.ToString()));
}
public Task<IEnumerable<string>> ListFiles()
{
var count = Math.Max(0, Math.Min(_client!.Count, _options.BatchSize));
return Task.FromResult(Enumerable.Range(0, count).Select(i => i.ToString()));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SW.Bitween.NativeAdapters/Pop3Receiver/NativePop3Receiver.cs` around lines 33
- 37, The ListFiles method in NativePop3Receiver can crash when
_options.BatchSize is negative because Math.Min(_client!.Count,
_options.BatchSize) may produce a negative count that gets passed into
Enumerable.Range. Update ListFiles to defensively clamp the computed count to
zero or otherwise ensure it is non-negative before calling Enumerable.Range,
keeping the behavior aligned with how the Rebex-based implementation tolerates
negative batch sizes.


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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔵 Trivial

No timeout/cancellation on any network call.

Initialize, ListFiles, GetFile, DeleteFile, and Finalize all invoke MailKit async calls without a CancellationToken. A hung/unreachable POP3 server blocks indefinitely, tying up the calling thread. Same pattern applies to NativeRebexPop3Receiver. Worth adding cancellation-token support (and a per-call timeout) at the INativeInfolinkReceiver boundary if this receiver runs on request/worker threads with SLAs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SW.Bitween.NativeAdapters/Pop3Receiver/NativePop3Receiver.cs` around lines 19
- 68, Add cancellation/timeout support to the POP3 network flow in
NativePop3Receiver so MailKit calls do not block indefinitely. Update
Initialize, ListFiles, GetFile, DeleteFile, and Finalize to accept and pass a
CancellationToken (or use a bounded timeout) through _client.ConnectAsync,
AuthenticateAsync, GetMessageAsync, DeleteMessageAsync, and DisconnectAsync.
Make the same change pattern in NativeRebexPop3Receiver and propagate token
support from the INativeInfolinkReceiver boundary.


public string Name => "NativePop3Receiver";

public void InitializeStartupValues(IDictionary<string, string> settings)
{
_options = settings.ConvertTo<Pop3ReceiverInput>();
}

public Type StartupValuesType => typeof(Pop3ReceiverInput);
}
23 changes: 23 additions & 0 deletions SW.Bitween.NativeAdapters/Pop3Receiver/Pop3ReceiverInput.cs
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

No bound validation on BatchSize.

Negative values pass through unvalidated and cause NativePop3Receiver.ListFiles (Line 35) to crash via Enumerable.Range(0, count). Consider adding [Range(1, int.MaxValue)].

Proposed fix
+    [Range(1, int.MaxValue)]
     [DefaultValue(50)]
     public int BatchSize { get; set; } = 50;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
[DefaultValue(50)]
public int BatchSize { get; set; } = 50;
[Range(1, int.MaxValue)]
[DefaultValue(50)]
public int BatchSize { get; set; } = 50;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SW.Bitween.NativeAdapters/Pop3Receiver/Pop3ReceiverInput.cs` around lines 18
- 19, The BatchSize property in Pop3ReceiverInput currently has no validation,
so negative values can flow into NativePop3Receiver.ListFiles and break
Enumerable.Range. Add a positive-range validation attribute to BatchSize (for
example, a minimum of 1) and keep the existing default value intact so invalid
input is rejected before it reaches ListFiles.


[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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Same partial-init leak risk as NativePop3Receiver.

If LoginAsync throws after ConnectAsync succeeds, _pop3 stays connected but the exception unwinds past any explicit cleanup; wrap in try/catch and dispose on failure, mirroring the fix suggested for NativePop3Receiver.Initialize.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SW.Bitween.NativeAdapters/RebexPop3Receiver/NativeRebexPop3Receiver.cs`
around lines 19 - 26, Initialize in NativeRebexPop3Receiver has the same
partial-initialization leak risk as NativePop3Receiver: if _pop3.ConnectAsync
succeeds and _pop3.LoginAsync fails, the connected Pop3 instance is left
undisposed. Update Initialize to wrap the connection/login sequence in
try/catch, and on any failure dispose or disconnect _pop3 before rethrowing so
the method leaves no partially initialized connection behind. Use the existing
_pop3, ConnectAsync, and LoginAsync flow as the fix point.


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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 -80

Repository: 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.cs

Repository: simplify9/Bitween-api

Length of output: 15532


🌐 Web query:

Rebex Pop3 GetMessageAsync overload string parameter local file path documentation GetMessage(int, string) attachment filename

💡 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
GetMessageAsync(sequenceNumber, attachment.FileName) writes the full message to the local path passed in attachment.FileName. That value is sender-controlled, so this can overwrite arbitrary files or escape the intended directory. The attachment bytes are already available from attachment.GetContentStream(), so this call is unnecessary.

Proposed fix
-        await _pop3.GetMessageAsync(sequenceNumber, attachment.FileName);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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();
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 using var stream = attachment.GetContentStream();
using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream);
var buffer = memoryStream.ToArray();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SW.Bitween.NativeAdapters/RebexPop3Receiver/NativeRebexPop3Receiver.cs`
around lines 43 - 57, The GetFile method in NativeRebexPop3Receiver is
performing an unnecessary raw-message download via
_pop3.GetMessageAsync(sequenceNumber, attachment.FileName), which can write to a
sender-controlled path. Remove that call and keep using
attachment.GetContentStream() in GetFile to read the attachment bytes directly,
preserving the existing XchangeFile flow without touching the local filesystem.


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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate DTO shape with Pop3ReceiverInput.

Identical to SW.Bitween.NativeAdapters/Pop3Receiver/Pop3ReceiverInput.cs. Consider a shared base (e.g. Pop3ReceiverInputBase) with Host/Username/Password/BatchSize/ResponseEncoding to keep the two adapters' contracts in sync as they evolve.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SW.Bitween.NativeAdapters/RebexPop3Receiver/RebexPop3ReceiverInput.cs` around
lines 6 - 23, `RebexPop3ReceiverInput` is duplicating the same DTO contract as
`Pop3ReceiverInput`, so extract the shared fields into a common base type and
have both adapter inputs inherit from it. Move the `Host`, `Username`,
`Password`, `BatchSize`, and `ResponseEncoding` properties (with their existing
attributes/defaults) into a shared class such as `Pop3ReceiverInputBase`, then
keep `RebexPop3ReceiverInput` and `Pop3ReceiverInput` as thin wrappers to
preserve their adapter-specific types.

7 changes: 7 additions & 0 deletions SW.Bitween.NativeAdapters/SW.Bitween.NativeAdapters.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,16 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="SW.Bitween.UnitTests" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="DotLiquid" Version="2.2.692" />
<PackageReference Include="MailKit" Version="4.17.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Rebex.Mail" Version="6.0.8060" />
<PackageReference Include="Rebex.Pop3" Version="6.0.8060" />
<PackageReference Include="Scriban" Version="7.0.6" />
<PackageReference Include="SimplyWorks.PrimitiveTypes" Version="8.1.2" />
</ItemGroup>
Expand Down
11 changes: 11 additions & 0 deletions SW.Bitween.NativeAdapters/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using SW.Bitween.NativeAdapters.HttpReceiver;
using SW.Bitween.NativeAdapters.Pop3Receiver;
using SW.Bitween.NativeAdapters.RebexPop3Receiver;

namespace SW.Bitween.NativeAdapters;

Expand Down Expand Up @@ -30,5 +32,14 @@ public static void AddNativeAdapters(this IServiceCollection serviceCollection)

serviceCollection.AddScoped<INativeInfolinkReceiver, NativeHttpReceiver>();
serviceCollection.AddScoped<INativeAdapter, NativeHttpReceiver>();

serviceCollection.AddScoped<INativeInfolinkReceiver, NativePop3Receiver>();
serviceCollection.AddScoped<INativeAdapter, NativePop3Receiver>();

if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(NativeRebexPop3Receiver.LicenseKeyEnvironmentVariable)))
{
serviceCollection.AddScoped<INativeInfolinkReceiver, NativeRebexPop3Receiver>();
serviceCollection.AddScoped<INativeAdapter, NativeRebexPop3Receiver>();
}
}
}
Loading