Skip to content

POP 受信でメールをレコードに取り込む ​

第5版作成 最終更新 (日本時間)
確認バージョン1.5.3.01.5.7.01.5.8.1

プリザンターには 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 など)を任意のフォルダに配置します。

text
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 のどちらでも動作します。

powershell
[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 にします。

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"
  }
}
powershell
pwsh -File .\ReceivePop.ps1 -ConfigPath .\config.json

添付ファイルも取り込む ​

メールの添付ファイルは、レコード作成 API(/api/items/{サイトID}/create)の AttachmentsHash に Base64 で渡すと、作成と同時に添付ファイル項目へ登録できます。キーには、サイト設定で有効化した添付ファイル項目(AttachmentsA など)を指定します。受信スクリプトの $createBody を作る前に、次の処理を入れます。

powershell
$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 行を足します。

powershell
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 分ごとに繰り返す」を設定します。

text
プログラム: pwsh.exe
引数      : -NoProfile -File "C:\PleasanterPopReceiver\ReceivePop.ps1" -ConfigPath "C:\PleasanterPopReceiver\config.json"
開始間隔  : 5 分
bash
*/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 回の起動で処理する件数に上限を設け、ジョブを短時間で終わらせる
TLSPOP3S(ポート 995)の利用を推奨。UseSsl = false の場合は社内ネットワーク内に限定

WARNING

Pop3Client.DeleteMessage は、Disconnect(true) を呼んだ時点で初めてサーバに反映されます。スクリプトが途中で異常終了するとサーバ側にメールが残るため、finally で Disconnect(true) を呼び、重複チェックも合わせて実装しておくと安全です。

本体改修方式(バックグラウンドワーカー) ​

本体に組み込むと、パラメータファイルでサーバ単位の受信設定ができ、バックグラウンドワーカーで定期実行でき、既存のユーザー・サイト・通知の仕組みと統合できます。

WARNING

本体改修は、次期バージョンへのアップデート時にマージ作業が必要になります。改修箇所は最小限にとどめ、機能の有効/無効をパラメータで切り替えられるようにしておくのがおすすめです。

図を読み込み中…

構成要素役割
Parameters/Pop.json受信サーバ・サイト ID の紐づけ設定
PopReceiverPOP3 で受信し、レコードを作成する新規クラス
ワーカープリザンター起動時に開始される定期実行スレッド
既存 APIItemModel.CreateByApi(API と同じ作成処理)を再利用

設定ファイル Pop.json を新設する ​

App_Data/Parameters/ に POP 受信用の設定ファイルを追加します。複数アカウントを扱えるよう配列形式にしておくと扱いやすくなります。

json
{
  "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"
    }
  ]
}

設定クラスは既存のパラメータと同じパターンで作ります。

csharp
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 に読み込み行を追加します。

csharp
public static Pop Pop;
csharp
// 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)に合わせています。

csharp
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 で入れるので、別の処理は要りません。

csharp
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 を繰り返します。

csharp
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() の後に追加します。

csharp
try
{
    await BackgroundServerScriptUtilities.InitScheduleAsync();
}
catch (Exception ex)
{
    logger.LogError(ex, "Background Server Script Schedule Registration Failed");
}
PopBackgroundWorker.Start();   // 追加

動作確認 ​

  1. App_Data/Parameters/Pop.json を配置し、Enabled を true にする
  2. プリザンターを再起動する
  3. テストメールを受信させる
  4. Pop.json で指定した SiteId のサイトを開き、レコードが追加されていることを確認する
  5. SysLogs テーブルに例外が記録されていないことを確認する

SysLogs には PopReceiver の例外がそのまま記録されるため、接続失敗や認証失敗の原因を追跡できます。

導入時の注意 ​

  • 機能の有効/無効を Enabled フラグで切り替えられるようにしておく
  • 例外を握りつぶさず SysLogs に記録する
  • サブモジュールとして上流のバージョンを追跡しやすくしておく

2 つの方式の比較 ​

観点外部ツール方式本体改修方式
改修範囲プリザンター本体に変更なし本体に新規クラスとワーカーを追加
アップデート影響ほぼなしバージョンアップ時にマージ作業が必要
設定管理外部 JSON ファイルApp_Data/Parameters/Pop.json で集中管理
定期実行タスクスケジューラ / cronプリザンター内部のバックグラウンドスレッド
サイトとの統合API 経由内部 API を直接呼び出し
添付ファイルcreate API の AttachmentsHashCreateByApi に渡す AttachmentsHash
認証情報の保管外部 JSON ファイルParameters 配下
拡張性スクリプトで自由に拡張(フィルタ、変換、別サイト振り分けなど)本体の API 処理(入力チェック・権限・通知)をそのまま使える

外部ツール方式は、スケジューラの管理が別途必要なこと、スクリプトの起動・接続のオーバーヘッドがありリアルタイム性が低いこと、API キーを外部ファイルで管理するため保管に注意が必要なことがデメリットです。リアルタイム性が必要、配信先サイトを動的に切り替えたい、本体機能として正式に組み込みたい、といった要件があれば本体改修方式を検討します。両方式を組み合わせて、メールサーバごとに使い分ける運用も可能です。

関連ページ ​

変更履歴

第5版記事の確認版を繰り返す表現を整理する
第4版外部連携の改修・設計メモ(iCal・RSS/Atom・Webhook 送受信・iPaaS・短縮 URL・POP 受信・マスターデータ同期)を追加
第3版「外部連携・AI」を 1.5.8.1 のソースで検証して修正
第2版記事のファイル名に並び順の番号を付け、元記事リンクを frontmatter の sources に移行
第1版「外部連携・AI」に NocoDB・Apache Superset・POP 受信を追加し、Fess の Azure 構築と Chatwork ログ検索を追記