POP 受信でメールをレコードに取り込む
プリザンターには SMTP でメールを送信する仕組みはありますが、メールを受信してレコードに取り込む機能は標準では用意されていません。問い合わせメールを記録テーブルに自動登録したい、といった要件に対しては次の 2 通りの方式があります。
| 方式 | 概要 | 対象バージョン |
|---|---|---|
| 外部ツール方式 | 本体には手を入れず、PowerShell スクリプトが POP3 で受信してプリザンターの API でレコードを作る | 1.5.3.0 |
| 本体改修方式 | 本体に受信クラスと定期実行ワーカーを追加し、内部 API で直接レコードを作る | 1.5.7.0 |
要件次第ですが、まず外部ツール方式で試してから本体改修に進むのがおすすめです。外部ツール方式で必要なパラメータや処理の流れを実機で固めておくと、本体改修時のクラス分割や設定項目の設計が決めやすくなります。テナントごとに受信箱を持たせ、本体の Quartz タイマーで動かす、より本格的な組み込みの設計は POP 受信の本体組み込み(Quartz タイマー・テナント設定)の設計 にまとめています。
受信先サイトを用意する
どちらの方式でも、メールを受け取る記録テーブル(または期限付きテーブル)を用意します。例として「問い合わせ管理」という記録テーブルに次の項目を持たせます。
| 項目名 | 種別 | 用途 |
|---|---|---|
タイトル | 既定 | メールの Subject |
内容 | 既定 | メール本文 |
分類A | 分類 | 送信元アドレス(From) |
日付A | 日付 | 受信日時(Date ヘッダ) |
分類B | 分類 | Message-Id(重複取り込み防止用キー) |
重複取り込みを防ぐため、Message-Id を保存する項目を 1 つ用意しておくのがおすすめです。取り込み前にこの項目で検索し、既に存在すればスキップします。
POP3 クライアント(MailKit)
.NET の標準ライブラリには POP3 クライアントが含まれていません。プリザンター本体でも採用されている MailKit(POP3 / POP3S / IMAP / SMTP に対応した OSS のクライアントライブラリ)を使うのが現実的です。1.5.8.1 の本体は MailKit 4.17.0 を参照しています(Implem.Pleasanter.csproj)。本体が使っているのは SMTP の送信だけで、Pop3Client / ImapClient を使うコードはありません。
- 外部ツール方式では、PowerShell から
Add-Typeで読み込みます - 本体改修方式では、メール送信機能の依存パッケージとして既に含まれているため、追加インストールは不要です
外部ツール方式(PowerShell + API)
図を読み込み中…
| 構成要素 | 役割 |
|---|---|
| メールサーバ | 受信するメールボックス(POP3 / POP3S が必要) |
| 受信スクリプト | POP3 で受信 → 本文・添付を解析 → プリザンター API を呼び出す |
| スケジューラ | スクリプトを定期実行する(Windows タスクスケジューラ、cron など) |
API キーはサイト管理者のユーザー設定から発行し、スクリプトの設定ファイルに書きます。
ファイル配置
NuGet から MailKit をダウンロードして展開し、MailKit.dll と依存 DLL(MimeKit.dll、BouncyCastle.Cryptography.dll など)を任意のフォルダに配置します。
C:\PleasanterPopReceiver\
├── ReceivePop.ps1
└── lib\
├── MailKit.dll
├── MimeKit.dll
└── BouncyCastle.Cryptography.dll受信スクリプト
MailKit の Pop3Client でメールを取得し、Message-Id(ClassB)で既存レコードを検索して重複をスキップしたうえで、/api/items/{サイトID}/create でレコードを作成します。Invoke-RestMethod だけで完結するため、PowerShell が動く環境であれば Windows / Linux のどちらでも動作します。
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][string]$ConfigPath
)
$ErrorActionPreference = "Stop"
# 設定ファイルを読み込む
$config = Get-Content -Path $ConfigPath -Raw | ConvertFrom-Json
# MailKit を読み込む
$libDir = Join-Path $PSScriptRoot "lib"
Add-Type -Path (Join-Path $libDir "BouncyCastle.Cryptography.dll")
Add-Type -Path (Join-Path $libDir "MimeKit.dll")
Add-Type -Path (Join-Path $libDir "MailKit.dll")
# POP3 で接続
$client = [MailKit.Net.Pop3.Pop3Client]::new()
$client.Connect($config.Pop.Host, $config.Pop.Port, $config.Pop.UseSsl)
$client.Authenticate($config.Pop.User, $config.Pop.Password)
try {
$count = $client.GetMessageCount()
Write-Host "受信件数: $count"
for ($i = 0; $i -lt $count; $i++) {
$message = $client.GetMessage($i)
# 重複チェック(Message-Id でプリザンターを検索)
$existsUri = "$($config.Pleasanter.BaseUrl)/api/items/$($config.Pleasanter.SiteId)/get"
$existsBody = @{
ApiVersion = 1.1
ApiKey = $config.Pleasanter.ApiKey
View = @{ ColumnFilterHash = @{ ClassB = $message.MessageId } }
} | ConvertTo-Json -Depth 10
$existsRes = Invoke-RestMethod -Method Post -Uri $existsUri `
-ContentType "application/json; charset=utf-8" -Body $existsBody
if ($existsRes.Response.Data.Count -gt 0) {
Write-Host "skip (duplicate): $($message.MessageId)"
continue
}
# 記録テーブルへ作成
$createUri = "$($config.Pleasanter.BaseUrl)/api/items/$($config.Pleasanter.SiteId)/create"
$createBody = @{
ApiVersion = 1.1
ApiKey = $config.Pleasanter.ApiKey
Title = $message.Subject
Body = $message.TextBody
ClassHash = @{
ClassA = $message.From.ToString()
ClassB = $message.MessageId
}
DateHash = @{ DateA = $message.Date.UtcDateTime.ToString("o") }
} | ConvertTo-Json -Depth 10
$createRes = Invoke-RestMethod -Method Post -Uri $createUri `
-ContentType "application/json; charset=utf-8" -Body $createBody
$recordId = $createRes.Response.Id
Write-Host "created: $recordId ($($message.Subject))"
# サーバから削除(必要に応じて)
if ($config.Pop.DeleteAfterReceive) {
$client.DeleteMessage($i)
}
}
}
finally {
$client.Disconnect($true)
$client.Dispose()
}設定ファイルは次のような JSON にします。
{
"Pop": {
"Host": "pop.example.com",
"Port": 995,
"UseSsl": true,
"User": "inquiry@example.com",
"Password": "your-password",
"DeleteAfterReceive": false
},
"Pleasanter": {
"BaseUrl": "https://pleasanter.example.com",
"SiteId": 123,
"ApiKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
}pwsh -File .\ReceivePop.ps1 -ConfigPath .\config.json添付ファイルも取り込む
メールの添付ファイルは、レコード作成 API(/api/items/{サイトID}/create)の AttachmentsHash に Base64 で渡すと、作成と同時に添付ファイル項目へ登録できます。キーには、サイト設定で有効化した添付ファイル項目(AttachmentsA など)を指定します。受信スクリプトの $createBody を作る前に、次の処理を入れます。
$attachments = @()
foreach ($attachment in $message.Attachments) {
if (-not $attachment.IsAttachment) { continue }
$ms = [System.IO.MemoryStream]::new()
try {
$attachment.Content.DecodeTo($ms)
$attachments += @{
Name = $attachment.FileName
ContentType = $attachment.ContentType.MimeType
Base64 = [System.Convert]::ToBase64String($ms.ToArray())
}
}
finally {
$ms.Dispose()
}
}$createBody のハッシュテーブルに次の 1 行を足します。
AttachmentsHash = @{ AttachmentsA = $attachments }Base64 は API で受け付ける添付ファイルのプロパティです(Attachment.cs)。/api/items/{レコードID}/binaries/multiupload という API は存在しない(binaries/multiupload は画面の添付ファイル項目が使う経路です)ため、AttachmentsHash を使う形に修正しています。Base64 にせずファイルだけを送りたい場合は ファイルのみのアップロード の /api/binaries/upload を使います。
定期実行
スクリプトを Windows タスクスケジューラ(または Linux の cron)に登録します。5 分間隔なら、タスクスケジューラのトリガーで「5 分ごとに繰り返す」を設定します。
プログラム: pwsh.exe
引数 : -NoProfile -File "C:\PleasanterPopReceiver\ReceivePop.ps1" -ConfigPath "C:\PleasanterPopReceiver\config.json"
開始間隔 : 5 分*/5 * * * * /usr/bin/pwsh -File /opt/pleasanter-pop/ReceivePop.ps1 -ConfigPath /opt/pleasanter-pop/config.json >> /var/log/pleasanter-pop.log 2>&1運用上の注意点
| 項目 | 注意点 |
|---|---|
| 重複取り込み | Message-Id を保存して取り込み前に検索する |
| 認証情報 | config.json を読み取り権限の限定されたフォルダに置く |
| 文字コード | MailKit が Content-Type を自動判別するため、TextBody の文字化け対策は不要 |
| エラー時 | サーバからの削除(DeleteMessage)はループ完了後の Disconnect(true) で確定する |
| HTML メール | message.TextBody が空のときは message.HtmlBody をフォールバックで使う |
| 大量受信 | 1 回の起動で処理する件数に上限を設け、ジョブを短時間で終わらせる |
| TLS | POP3S(ポート 995)の利用を推奨。UseSsl = false の場合は社内ネットワーク内に限定 |
WARNING
Pop3Client.DeleteMessage は、Disconnect(true) を呼んだ時点で初めてサーバに反映されます。スクリプトが途中で異常終了するとサーバ側にメールが残るため、finally で Disconnect(true) を呼び、重複チェックも合わせて実装しておくと安全です。
本体改修方式(バックグラウンドワーカー)
本体に組み込むと、パラメータファイルでサーバ単位の受信設定ができ、バックグラウンドワーカーで定期実行でき、既存のユーザー・サイト・通知の仕組みと統合できます。
WARNING
本体改修は、次期バージョンへのアップデート時にマージ作業が必要になります。改修箇所は最小限にとどめ、機能の有効/無効をパラメータで切り替えられるようにしておくのがおすすめです。
図を読み込み中…
| 構成要素 | 役割 |
|---|---|
Parameters/Pop.json | 受信サーバ・サイト ID の紐づけ設定 |
PopReceiver | POP3 で受信し、レコードを作成する新規クラス |
| ワーカー | プリザンター起動時に開始される定期実行スレッド |
| 既存 API | ItemModel.CreateByApi(API と同じ作成処理)を再利用 |
設定ファイル Pop.json を新設する
App_Data/Parameters/ に POP 受信用の設定ファイルを追加します。複数アカウントを扱えるよう配列形式にしておくと扱いやすくなります。
{
"Enabled": true,
"IntervalSeconds": 300,
"Accounts": [
{
"Name": "Inquiry",
"Host": "pop.example.com",
"Port": 995,
"UseSsl": true,
"User": "inquiry@example.com",
"Password": "your-password",
"DeleteAfterReceive": false,
"TenantId": 1,
"UserId": 1,
"SiteId": 123,
"MessageIdColumn": "ClassB",
"FromColumn": "ClassA",
"DateColumn": "DateA",
"AttachmentsColumn": "AttachmentsA"
}
]
}設定クラスは既存のパラメータと同じパターンで作ります。
namespace Implem.ParameterAccessor.Parts
{
public class Pop
{
public bool Enabled;
public int IntervalSeconds = 300;
public PopAccount[] Accounts;
}
public class PopAccount
{
public string Name;
public string Host;
public int Port = 995;
public bool UseSsl = true;
public string User;
public string Password;
public bool DeleteAfterReceive;
public int TenantId;
public int UserId;
public long SiteId;
public string MessageIdColumn;
public string FromColumn;
public string DateColumn;
public string AttachmentsColumn;
}
}Parameters クラスにフィールドを、Initializer に読み込み行を追加します。
public static Pop Pop;// SetParameters() の中の Read<T>() が並んでいる箇所に追加
Parameters.Pop = Read<Pop>(required: false);Read<T>() は型名(Pop)から App_Data/Parameters/Pop.json を探して読み込みます。ファイルが無い環境でも起動できるよう required: false を付けておくと安全です。これで Parameters.Pop で設定値にアクセスできます。
PopReceiver の実装
アカウントごとに受信し、例外は SysLogModel に記録して次のアカウントへ進みます。バックグラウンドには HTTP リクエストが無いので、レコードの作成者にするユーザー(Pop.json の TenantId / UserId)で Context を作ります。作り方は、バックグラウンドサーバースクリプトから API を呼ぶときの本体の処理(ServerScriptUtilities.cs)に合わせています。
using Implem.DefinitionAccessor;
using Implem.Libraries.Utilities;
using Implem.ParameterAccessor.Parts;
using Implem.Pleasanter.Libraries.Requests;
using Implem.Pleasanter.Libraries.Security;
using Implem.Pleasanter.Models;
using MailKit.Net.Pop3;
using MimeKit;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Implem.Pleasanter.Libraries.Mails
{
public static class PopReceiver
{
public static void ReceiveAll()
{
var parameter = Parameters.Pop;
if (parameter?.Enabled != true || parameter.Accounts == null) return;
foreach (var account in parameter.Accounts)
{
var context = CreateContext(account: account);
try
{
Receive(context: context, account: account);
}
catch (Exception e)
{
new SysLogModel(context: context, e: e);
}
}
}
private static Context CreateContext(PopAccount account)
{
var context = new Context(
tenantId: account.TenantId,
userId: account.UserId,
request: false,
setAuthenticated: true);
context.Controller = "items";
context.Action = "create";
context.Id = account.SiteId;
context.PermissionHash = Permissions.Get(context: context);
return context;
}
private static void Receive(Context context, PopAccount account)
{
using var client = new Pop3Client();
client.Connect(account.Host, account.Port, account.UseSsl);
client.Authenticate(account.User, account.Password);
try
{
var count = client.GetMessageCount();
for (var i = 0; i < count; i++)
{
var message = client.GetMessage(i);
if (Exists(context: context, account: account, messageId: message.MessageId))
{
continue;
}
var created = CreateRecord(
context: context,
account: account,
message: message);
if (created && account.DeleteAfterReceive)
{
client.DeleteMessage(i);
}
}
}
finally
{
client.Disconnect(quit: true);
}
}
}
}Exists は、MessageIdColumn の列に同じ Message-Id を持つレコードがあるかを調べる処理として実装します(テーブルの種類に応じて Issues / Results テーブルを SiteId と列の値で検索します)。
レコードと添付ファイルの作成
レコードは、API と同じ JSON を context.ApiRequestBody に入れて ItemModel.CreateByApi を呼び出して作成します(ItemModel.cs)。期限付きテーブルか記録テーブルかによる振り分けは CreateByApi の中で行われます。添付ファイルは外部ツール方式と同じく AttachmentsHash に Base64 で入れるので、別の処理は要りません。
private static bool CreateRecord(Context context, PopAccount account, MimeMessage message)
{
var body = !string.IsNullOrEmpty(message.TextBody)
? message.TextBody
: message.HtmlBody ?? string.Empty;
var request = new Dictionary<string, object>
{
["Title"] = message.Subject,
["Body"] = body,
["ClassHash"] = new Dictionary<string, string>
{
[account.FromColumn] = message.From.ToString(),
[account.MessageIdColumn] = message.MessageId
},
["DateHash"] = new Dictionary<string, string>
{
[account.DateColumn] = message.Date.UtcDateTime.ToString("o")
}
};
if (!string.IsNullOrEmpty(account.AttachmentsColumn))
{
var attachments = message.Attachments
.OfType<MimePart>()
.Where(part => part.IsAttachment)
.Select(part =>
{
using var ms = new MemoryStream();
part.Content.DecodeTo(ms);
return new Dictionary<string, string>
{
["Name"] = part.FileName,
["ContentType"] = part.ContentType.MimeType,
["Base64"] = Convert.ToBase64String(ms.ToArray())
};
})
.ToList();
if (attachments.Any())
{
request["AttachmentsHash"] = new Dictionary<string, object>
{
[account.AttachmentsColumn] = attachments
};
}
}
context.ApiRequestBody = request.ToJson();
var result = new ItemModel(
context: context,
referenceId: account.SiteId)
.CreateByApi(context: context);
return result.StatusCode == 200;
}INFO
辞書を受け取る IssueUtilities.Create / ResultUtilities.Create や BinaryUtilities.UpdateRecordAttachments は本体に存在しないため、既存の ItemModel.CreateByApi を呼ぶ形に修正しています。CreateByApi を通すと、API と同じ入力チェック・権限チェック・通知が働きます。
ワーカー(定期実行)の組み込み
バックグラウンドのスレッドを起動し、while ループの中で PopReceiver.ReceiveAll と Thread.Sleep を繰り返します。
using Implem.DefinitionAccessor;
using System;
using System.Threading;
namespace Implem.Pleasanter.Libraries.Mails
{
public static class PopBackgroundWorker
{
public static void Start()
{
if (Parameters.Pop?.Enabled != true) return;
var interval = Math.Max(60, Parameters.Pop.IntervalSeconds) * 1000;
new Thread(() =>
{
while (true)
{
try
{
PopReceiver.ReceiveAll();
}
catch (Exception e)
{
Console.Error.WriteLine(e);
}
Thread.Sleep(interval);
}
})
{
IsBackground = true,
Name = "PopBackgroundWorker"
}.Start();
}
}
}IntervalSeconds の最小値を 60 に丸めることで、設定誤りによる過剰なポーリングを防いでいます。アカウントごとの例外は ReceiveAll の中で SysLogs に記録されるので、ここで拾うのはそれ以外の例外だけです。
起動位置は Startup.ConfigureServices() の直下ではありません。確認したソースでは、TimerBackground とバックグラウンドサーバースクリプトのスケジュール登録は、Startup.ConfigureServices() で登録されるホステッドサービス CustomQuartzHostedService の ExecuteAsync の中で、起動準備(ウォームアップ)の完了を待ってから行われます(Startup.cs、CustomQuartzHostedService.cs)。これに合わせて、BackgroundServerScriptUtilities.InitScheduleAsync() の後に追加します。
try
{
await BackgroundServerScriptUtilities.InitScheduleAsync();
}
catch (Exception ex)
{
logger.LogError(ex, "Background Server Script Schedule Registration Failed");
}
PopBackgroundWorker.Start(); // 追加動作確認
App_Data/Parameters/Pop.jsonを配置し、Enabledをtrueにする- プリザンターを再起動する
- テストメールを受信させる
Pop.jsonで指定したSiteIdのサイトを開き、レコードが追加されていることを確認する- SysLogs テーブルに例外が記録されていないことを確認する
SysLogs には PopReceiver の例外がそのまま記録されるため、接続失敗や認証失敗の原因を追跡できます。
導入時の注意
- 機能の有効/無効を
Enabledフラグで切り替えられるようにしておく - 例外を握りつぶさず SysLogs に記録する
- サブモジュールとして上流のバージョンを追跡しやすくしておく
2 つの方式の比較
| 観点 | 外部ツール方式 | 本体改修方式 |
|---|---|---|
| 改修範囲 | プリザンター本体に変更なし | 本体に新規クラスとワーカーを追加 |
| アップデート影響 | ほぼなし | バージョンアップ時にマージ作業が必要 |
| 設定管理 | 外部 JSON ファイル | App_Data/Parameters/Pop.json で集中管理 |
| 定期実行 | タスクスケジューラ / cron | プリザンター内部のバックグラウンドスレッド |
| サイトとの統合 | API 経由 | 内部 API を直接呼び出し |
| 添付ファイル | create API の AttachmentsHash | CreateByApi に渡す AttachmentsHash |
| 認証情報の保管 | 外部 JSON ファイル | Parameters 配下 |
| 拡張性 | スクリプトで自由に拡張(フィルタ、変換、別サイト振り分けなど) | 本体の API 処理(入力チェック・権限・通知)をそのまま使える |
外部ツール方式は、スケジューラの管理が別途必要なこと、スクリプトの起動・接続のオーバーヘッドがありリアルタイム性が低いこと、API キーを外部ファイルで管理するため保管に注意が必要なことがデメリットです。リアルタイム性が必要、配信先サイトを動的に切り替えたい、本体機能として正式に組み込みたい、といった要件があれば本体改修方式を検討します。両方式を組み合わせて、メールサーバごとに使い分ける運用も可能です。