add ProxyJump function
This commit is contained in:
1 parent
56e20e5464
commit
7f0e0fdb23
48 files changed
+3875
-44
No files matched your search
@@ -0,0 +1,287 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace BulkArchiveMailer.Core
|
||||
{
|
||||
/// <summary>Outcome recorded for one send attempt.</summary>
|
||||
public static class SendLogStatus
|
||||
{
|
||||
public const string Sent = "Sent";
|
||||
public const string Failed = "Failed";
|
||||
|
||||
/// <summary>Written in test mode (mail saved to Drafts, not actually sent) - deliberately NOT "Sent", so a later real send is not treated as a duplicate.</summary>
|
||||
public const string Draft = "Draft";
|
||||
}
|
||||
|
||||
public sealed class SendLogEntry
|
||||
{
|
||||
public DateTime TimestampUtc { get; set; }
|
||||
public string Login { get; set; } = "";
|
||||
public string Email { get; set; } = "";
|
||||
public ArchiveKind Kind { get; set; }
|
||||
public string ArchivePath { get; set; } = "";
|
||||
public string FileSha256 { get; set; } = "";
|
||||
public string Status { get; set; } = "";
|
||||
public string Error { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Append-only CSV log at a path chosen by the caller (AddIn typically
|
||||
/// keeps it next to the archive folder, e.g.
|
||||
/// "<archiveFolder>\send-log.csv"). Columns:
|
||||
/// TimestampUtc,Login,Email,Kind,ArchivePath,FileSha256,Status,Error
|
||||
///
|
||||
/// Dedup key for "already sent this exact archive": (Email, Kind,
|
||||
/// FileSha256) with Status=Sent. Hashing the archive's *current* bytes
|
||||
/// (done by the caller via ComputeSha256, at scan time) is what lets a
|
||||
/// regenerated archive - same login, same kind, new key material or
|
||||
/// port list - be correctly treated as NOT already sent, while a
|
||||
/// second scan of an untouched folder correctly flags it as
|
||||
/// AlreadySent.
|
||||
/// </summary>
|
||||
public sealed class SendLogStore
|
||||
{
|
||||
private static readonly string[] Header =
|
||||
{
|
||||
"TimestampUtc", "Login", "Email", "Kind", "ArchivePath", "FileSha256", "Status", "Error",
|
||||
};
|
||||
|
||||
public string CsvPath { get; }
|
||||
|
||||
public SendLogStore(string csvPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(csvPath))
|
||||
{
|
||||
throw new ArgumentException("csvPath must not be empty.", nameof(csvPath));
|
||||
}
|
||||
|
||||
CsvPath = csvPath;
|
||||
}
|
||||
|
||||
public static string ComputeSha256(string filePath)
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
using (var stream = File.OpenRead(filePath))
|
||||
{
|
||||
var hash = sha.ComputeHash(stream);
|
||||
var sb = new StringBuilder(hash.Length * 2);
|
||||
foreach (var b in hash)
|
||||
{
|
||||
sb.Append(b.ToString("x2", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<SendLogEntry> ReadAll()
|
||||
{
|
||||
var result = new List<SendLogEntry>();
|
||||
if (!File.Exists(CsvPath))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
using (var reader = new StreamReader(CsvPath, Encoding.UTF8))
|
||||
{
|
||||
var first = true;
|
||||
string line;
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
if (first)
|
||||
{
|
||||
first = false;
|
||||
continue; // header row
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var fields = ParseCsvLine(line);
|
||||
if (fields.Count < Header.Length)
|
||||
{
|
||||
continue; // malformed/truncated row - skip rather than throw
|
||||
}
|
||||
|
||||
ArchiveKind kind;
|
||||
Enum.TryParse(fields[3], true, out kind);
|
||||
|
||||
DateTime ts;
|
||||
DateTime.TryParse(
|
||||
fields[0],
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.RoundtripKind | DateTimeStyles.AssumeUniversal,
|
||||
out ts);
|
||||
|
||||
result.Add(new SendLogEntry
|
||||
{
|
||||
TimestampUtc = ts,
|
||||
Login = fields[1],
|
||||
Email = fields[2],
|
||||
Kind = kind,
|
||||
ArchivePath = fields[4],
|
||||
FileSha256 = fields[5],
|
||||
Status = fields[6],
|
||||
Error = fields[7],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void Append(SendLogEntry entry)
|
||||
{
|
||||
if (entry == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(entry));
|
||||
}
|
||||
|
||||
var dir = Path.GetDirectoryName(CsvPath);
|
||||
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
|
||||
var writeHeader = !File.Exists(CsvPath);
|
||||
using (var writer = new StreamWriter(CsvPath, append: true, encoding: new UTF8Encoding(false)))
|
||||
{
|
||||
if (writeHeader)
|
||||
{
|
||||
writer.WriteLine(string.Join(",", Header));
|
||||
}
|
||||
|
||||
writer.WriteLine(string.Join(",", new[]
|
||||
{
|
||||
Escape(entry.TimestampUtc.ToString("o", CultureInfo.InvariantCulture)),
|
||||
Escape(entry.Login),
|
||||
Escape(entry.Email),
|
||||
Escape(entry.Kind.ToString()),
|
||||
Escape(entry.ArchivePath),
|
||||
Escape(entry.FileSha256),
|
||||
Escape(entry.Status),
|
||||
Escape(entry.Error),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if the most recent matching row for (email, kind, sha256)
|
||||
/// has Status=Sent. Draft/Failed rows never count as "already
|
||||
/// sent" - only a completed real Send does.
|
||||
/// </summary>
|
||||
public bool TryFindSent(string email, ArchiveKind kind, string sha256, out SendLogEntry match)
|
||||
{
|
||||
match = null;
|
||||
if (string.IsNullOrEmpty(sha256) || string.IsNullOrEmpty(email))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var entry in ReadAll())
|
||||
{
|
||||
if (!string.Equals(entry.Status, SendLogStatus.Sent, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.Equals(entry.Email, email, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.Kind != kind)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.Equals(entry.FileSha256, sha256, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (match == null || entry.TimestampUtc > match.TimestampUtc)
|
||||
{
|
||||
match = entry;
|
||||
}
|
||||
}
|
||||
|
||||
return match != null;
|
||||
}
|
||||
|
||||
private static string Escape(string field)
|
||||
{
|
||||
field = field ?? "";
|
||||
if (field.IndexOfAny(new[] { ',', '"', '\n', '\r' }) >= 0)
|
||||
{
|
||||
return "\"" + field.Replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
|
||||
return field;
|
||||
}
|
||||
|
||||
private static List<string> ParseCsvLine(string line)
|
||||
{
|
||||
var fields = new List<string>();
|
||||
var i = 0;
|
||||
var n = line.Length;
|
||||
|
||||
while (i < n)
|
||||
{
|
||||
string field;
|
||||
if (line[i] == '"')
|
||||
{
|
||||
i++;
|
||||
var sb = new StringBuilder();
|
||||
while (i < n)
|
||||
{
|
||||
if (line[i] == '"')
|
||||
{
|
||||
if (i + 1 < n && line[i + 1] == '"')
|
||||
{
|
||||
sb.Append('"');
|
||||
i += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(line[i]);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
field = sb.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
var start = i;
|
||||
while (i < n && line[i] != ',')
|
||||
{
|
||||
i++;
|
||||
}
|
||||
|
||||
field = line.Substring(start, i - start);
|
||||
}
|
||||
|
||||
fields.Add(field);
|
||||
if (i < n && line[i] == ',')
|
||||
{
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user