POP 受信の本体組み込み(Quartz タイマー・テナント設定)の設計
プリザンターはメールを SMTP で送信できますが、POP3 / IMAP で受信する機能はありません。外部スクリプトで取り込む方法と、パラメータファイルとスレッドで動かす簡易な本体改修は POP 受信でメールをレコードに取り込む にまとめています。
このページは、それをもう一歩進めて、本体の Quartz タイマーとテナント設定に組み込む場合の設計メモです。テナントごとに受信箱と取り込み先を設定でき、重複防止を専用テーブルで行います。本体の標準機能ではありません。
前提にした現行実装(1.5.8.1)
メール送信の基盤
| 項目 | 1.5.8.1 |
|---|---|
| ライブラリ | MailKit 4.17.0(Implem.Pleasanter.csproj)。POP3 / IMAP のクライアント(Pop3Client / ImapClient)も同じパッケージに入っているが、本体では使っていない |
| 送信 | Smtp.cs が SmtpClient で送る。Mail.json の SecureSocketOptions が解釈できればそれを使い、無ければ SmtpEnableSsl で StartTls か None を選ぶ。ServerCertificateValidationCallback が true なら証明書の検証を省く。UseOAuth なら OAuth2(SaslMechanismOAuth2)で認証する(Smtp.cs) |
| 送信記録 | OutgoingMails テーブル |
POP3 の接続・認証も、この SMTP と同じ考え方(ソケットの暗号化方式の解釈、OAuth2 の流用)で組めます。
Quartz のタイマー
| 項目 | 1.5.8.1 |
|---|---|
| 登録 | 各タイマーの Param(IExecutionTimerBaseParam)を TimerBackground の一覧に並べる(TimerBackground.cs) |
| 起動条件 | 一覧全体は BackgroundService.TimerEnabled(既存タイマーの有効フラグの OR)が true のときだけ動く(BackgroundService.cs)。新しいタイマーを足すときはここにも条件を足す |
| 時刻指定 | TimeList("02:00" のような HH:mm)を返すと毎日その時刻に動く |
| 間隔指定 | TimeList を null にして SetCustomTimer で SimpleSchedule を登録する。リマインダーは ReminderCheckIntervalSeconds(既定 60 秒)を 30〜3600 秒に丸め、範囲外なら SysLogs に警告を出す(ReminderBackgroundTimer.cs) |
| 多重実行の防止 | リマインダーは静的な IsRunning フラグで、前回の処理中なら何もしない |
レコードの作成
サーバースクリプトの items.Create の実体 ServerScriptUtilities.Create(context, id, model) は、API 用の Context を作って ItemModel.CreateByServerScript を呼びます(ServerScriptUtilities.cs)。
modelは JSON 文字列かServerScriptModelApiModelで渡す。記録テーブル・期限付きテーブルでそれ以外のオブジェクトを渡すとToString()の結果が本文になる(ItemModel.cs)ので、C# のDictionaryはそのまま渡せない- 作成の前にサイトの API 上限(
WithinApiLimits)を確認する(ItemModel.cs)
POP 受信でメールをレコードに取り込む の本体改修方式では、API と同じ入力チェック・権限チェック・通知を通すために ItemModel.CreateByApi を使っています。どちらを使っても、本文は API と同じ JSON 文字列にします。
全体の構成
図を読み込み中…
図を読み込み中…
パラメータ
サーバー全体のスイッチと間隔を BackgroundService.json に、受信箱ごとの設定はテナント設定に持たせます。
| パラメータ | 既定 | 説明 |
|---|---|---|
BackgroundService.PopMailPolling | false | 受信タイマーの有効・無効 |
BackgroundService.PopMailPollingIntervalSeconds | 300 | 受信の間隔(秒)。リマインダーと同じく下限・上限で丸める |
Mail.Pop3MaxMessagesPerPoll | 50 | 1 回に処理する最大件数 |
受信箱の設定をどこに置くかは 3 通り考えられます。
| 方式 | 利点 | 欠点 |
|---|---|---|
A. Mail.json に 1 つ | 最も簡単 | テナントごとに受信箱を分けられない |
| B. テナント設定(推奨) | テナントごとに受信箱と取り込み先を持てる | 設定画面が要る |
| C. サイト設定 | テーブルごとに設定できる | 設定が散らばる |
B は、バックグラウンドサーバースクリプトが TenantSettings.BackgroundServerScripts に入っているのと同じ形です(TenantSettings.cs)。
public class PopMailSetting : ISettingListItem
{
public int Id { get; set; }
public string Pop3Host;
public int Pop3Port = 995;
public string Pop3UserName;
public string Pop3Password;
public string Pop3SecureSocketOptions; // 例: SslOnConnect
public bool Pop3UseOAuth;
public long TargetSiteId; // 取り込み先
public int ExecutionUserId; // レコードの作成者
public bool DeleteAfterReceive;
public string SubjectFilter; // 件名に含む文字列
public string FromFilter; // 差出人に含む文字列
public PopMailColumnMapping ColumnMapping;
public bool Disabled;
}
public class PopMailColumnMapping
{
public string SubjectColumn = "Title";
public string BodyColumn = "Body";
public string FromColumn = "ClassA";
public string ToColumn = "ClassB";
public string DateColumn = "DateA";
public string MessageIdColumn = "ClassC";
public string CcColumn;
public string AttachmentsColumn; // 例: AttachmentsA
}受信
MailKit の Pop3Client で受けます。
| API | 内容 |
|---|---|
ConnectAsync / AuthenticateAsync | 接続と認証(パスワードまたは OAuth2) |
Count | サーバー上の件数 |
GetMessageUidAsync(i) | UID |
GetMessageAsync(i) | MimeMessage(From・To・Cc・Subject・Date・MessageId・TextBody・HtmlBody・Attachments・Headers) |
DeleteMessageAsync(i) | 削除の印を付ける |
DisconnectAsync(quit: true) | 切断。ここで削除が確定する |
using var client = new Pop3Client();
var options = Enum.TryParse<SecureSocketOptions>(setting.Pop3SecureSocketOptions, out var op)
? op
: SecureSocketOptions.SslOnConnect;
await client.ConnectAsync(setting.Pop3Host, setting.Pop3Port, options);
if (setting.Pop3UseOAuth)
{
await client.AuthenticateAsync(new SaslMechanismOAuth2(setting.Pop3UserName, accessToken));
}
else
{
await client.AuthenticateAsync(setting.Pop3UserName, setting.Pop3Password);
}
var count = Math.Min(client.Count, Parameters.Mail.Pop3MaxMessagesPerPoll);
for (var i = 0; i < count; i++)
{
var uid = await client.GetMessageUidAsync(i);
if (IsImported(setting, uid)) continue;
var message = await client.GetMessageAsync(i);
// フィルタ → レコード作成 → UID の記録 → 必要なら DeleteMessageAsync(i)
}
await client.DisconnectAsync(quit: true);重複取り込みの防止
| 方式 | 利点 | 欠点 |
|---|---|---|
| UID を専用テーブルで管理(推奨) | 確実。取り込み先の項目を使わない | テーブルが増える |
Message-ID を取り込み先の項目に保存して照合 | テーブル追加が要らない | 取り込み先に項目が要る |
| 取り込んだらサーバーから削除 | 最も簡単 | 失敗したときにメールを失うおそれ |
CREATE TABLE PopMailUids (
TenantId INT NOT NULL,
PopMailSettingId INT NOT NULL,
Uid NVARCHAR(512) NOT NULL,
MessageId NVARCHAR(998) NULL,
CreatedTime DATETIME NOT NULL,
CONSTRAINT PK_PopMailUids PRIMARY KEY (TenantId, PopMailSettingId, Uid)
);
CREATE INDEX IX_PopMailUids_CreatedTime ON PopMailUids (CreatedTime);実装では CodeDefiner の定義に追加します。古い UID(90 日以上前など)は定期的に削除します。フィルタで対象外にしたメールも UID を記録しておくと、毎回取得し直さずに済みます。
レコードへの対応付け
| メール | 既定の項目 |
|---|---|
| 件名 | タイトル |
本文(TextBody、空なら HtmlBody) | 内容 |
| 差出人 | 分類 A |
| 宛先 | 分類 B |
| 送信日時 | 日付 A |
Message-ID | 分類 C |
| 添付ファイル | 添付ファイル項目(AttachmentsHash に Base64 で渡す) |
実行ユーザー(ExecutionUserId)の Context は、バックグラウンドサーバースクリプトと同じ手順で作ります(手順は iCal フィードの設計 を参照)。レコードは API と同じ JSON を組み立て、文字列にしてから渡します。
var mapping = setting.ColumnMapping;
var request = new Dictionary<string, object>
{
["Title"] = mail.Subject,
["Body"] = mail.TextBody ?? mail.HtmlBody ?? string.Empty,
["ClassHash"] = new Dictionary<string, string>
{
[mapping.FromColumn] = mail.From.ToString(),
[mapping.ToColumn] = mail.To.ToString(),
[mapping.MessageIdColumn] = mail.MessageId
},
["DateHash"] = new Dictionary<string, string>
{
[mapping.DateColumn] = mail.Date.UtcDateTime.ToString("o")
}
};
// 添付があれば request["AttachmentsHash"] に { Name, ContentType, Base64 } の配列を入れる
ServerScriptUtilities.Create(
context: execContext,
id: setting.TargetSiteId,
model: request.ToJson());添付ファイルのサイズ上限は既存の添付ファイルの設定に従い、超えるものは飛ばして SysLogs に警告を残します。
フィルタ
| 項目 | 例 | 動作 |
|---|---|---|
SubjectFilter | [問い合わせ] | 件名にこの文字列を含むメールだけ取り込む |
FromFilter | @example.com | 差出人にこの文字列を含むメールだけ取り込む |
空なら全件を取り込み、両方あれば AND で判定します。
タイマー
class PopMailPollingTimer : ClusterExecutionTimerBase
{
static private bool IsRunning = false;
public class Param : IExecutionTimerBaseParam
{
public static readonly JobKey jobKey = new JobKey("PopMailPollingTimer", "ExecutionTimerBase");
public Type JobType => typeof(PopMailPollingTimer);
public IEnumerable<string> TimeList => null;
public bool Enabled => Parameters.BackgroundService.PopMailPolling;
public JobKey JobKey => jobKey;
public string JobName => "PopMailPollingService";
public async Task<bool> SetCustomTimer(IScheduler scheduler)
{
var seconds = Math.Clamp(Parameters.BackgroundService.PopMailPollingIntervalSeconds, 60, 3600);
var triggerKey = TimerTriggerRegistrar.SimpleTriggerKey(JobKey);
var trigger = TriggerBuilder.Create()
.WithIdentity(triggerKey)
.ForJob(JobKey)
.WithSimpleSchedule(x => x.WithIntervalInSeconds(seconds).RepeatForever())
.Build();
await TimerTriggerRegistrar.EnsureTriggerAsync(scheduler: scheduler, trigger: trigger);
return true;
}
}
public override async Task Execute(IJobExecutionContext context)
{
if (IsRunning) return;
await Task.Run(async () =>
{
if (IsRunning) return;
var sysContext = CreateContext();
try
{
IsRunning = true;
await PopMailPollingUtilities.PollAsync(context: sysContext);
}
catch (Exception e)
{
_ = new SysLogModel(context: sysContext, e: e, extendedErrorMessage: "PopMailPollingService Exception");
}
finally
{
IsRunning = false;
}
}, context.CancellationToken);
}
internal static IExecutionTimerBaseParam GetParam() => new Param();
}変更箇所
| ファイル | 内容 |
|---|---|
Implem.ParameterAccessor/Parts/BackgroundService.cs | PopMailPolling・PopMailPollingIntervalSeconds を追加し、TimerEnabled の条件にも足す |
Implem.ParameterAccessor/Parts/Mail.cs・Mail.json | Pop3MaxMessagesPerPoll |
App_Data/Parameters/BackgroundService.json | 既定値 |
Libraries/BackgroundServices/PopMailPollingTimer.cs | タイマー(新規) |
Libraries/BackgroundServices/PopMailPollingUtilities.cs | テナントごとの受信処理(新規) |
Libraries/BackgroundServices/TimerBackground.cs | 一覧に PopMailPollingTimer.GetParam() を追加 |
Libraries/Settings/TenantSettings.cs | PopMailSettings を追加 |
Models/Tenants/TenantUtilities.cs | テナントの管理画面に設定タブ |
| CodeDefiner の定義 | PopMailUids テーブル |
セキュリティ
| 項目 | 対策 |
|---|---|
| パスワードの保管 | テナント設定に平文で入れず暗号化する |
| 暗号化 | 既定は SslOnConnect(995 番) |
| OAuth2 | SMTP の OAuth2 の仕組みを流用する |
| 悪意のある内容 | HTML 本文の無害化、添付ファイルのウイルス検査との連携を検討する |
| 大量受信 | Pop3MaxMessagesPerPoll で 1 回の件数を制限する |
| 証明書の検証 | SMTP と同じ ServerCertificateValidationCallback で制御する |
代替案: バックグラウンドサーバースクリプトと中継サービス
C# の改修を避けるなら、POP3 を受けて JSON で返す小さな中継サービスを外に置き、バックグラウンドサーバースクリプトから httpClient で取りに行く方法もあります。
図を読み込み中…
// バックグラウンドサーバースクリプト(関数化をオン)
var targetSiteId = 12345;
httpClient.RequestUri = 'https://mail-bridge.example.com/api/mails';
var response = httpClient.Get();
if (!httpClient.IsSuccess) {
logs.LogInfo('メール取得に失敗: ' + httpClient.StatusCode);
return;
}
JSON.parse(response).forEach(function (mail) {
items.Create(targetSiteId, JSON.stringify({
Title: mail.subject,
Body: mail.body,
ClassHash: { ClassA: mail.from, ClassB: mail.to },
DateHash: { DateA: mail.date }
}));
});httpClient.Get() は応答の本文を戻り値で返します。バックグラウンドサーバースクリプトでは画面が無いので、記録は context.Log ではなく logs.LogInfo(SysLogs)に残します(RSS リーダー も参照)。
| 観点 | 本体に組み込む | 中継サービス + スクリプト |
|---|---|---|
| 開発量 | 大きい(C#・テーブル追加) | 小さい |
| 保守 | バージョンアップのたびに追従 | 本体と独立 |
| 信頼性 | 本体のログ・障害管理に乗る | 中継サービスの可用性に依存 |
| 添付ファイル | そのまま扱える | 中継側で Base64 にする |
| テナント分離 | テナント設定で自然に分かれる | 中継側で管理する |