2025/10/05(日)Gmailにも届くメーリングリスト-さくらインターネット
簡便法、Gmailにも届くさくらインターネットのメーリングリストの改良スクリプト
さくらインターネットMLでFromを書き換えてSPFalignmentを成功させる で紹介したスクリプトは複雑で怖い、という方向け、最小限のFrom書換&replt-to 書換スクリプトです。スクリプトの組み込み方法は、fmlにスクリプトを組み込む方法 をご参照ください。
# YOU CAN EDIT MANUALLY AFTER HERE. $START_HOOK = q{ # Reply-To header processing { my $rt = &GET_HEADER_FIELD_VALUE('reply-to'); $rt = '' unless defined $rt; if (&MailListMemberP($From_address)) { unless (length $rt) { &DEFINE_FIELD_FORCED("reply-to", $MAIL_LIST); } } else { if (length $rt) { &DEFINE_FIELD_FORCED("reply-to", $rt . "," . $MAIL_LIST); } else { my $fa = defined($From_address) ? $From_address : ''; &DEFINE_FIELD_FORCED("reply-to", $fa . "," . $MAIL_LIST); } } } # From header rewrite ("<name> via ML:" <MAIL_LIST>) { # 1) read original From safely my $orig_from = &GET_HEADER_FIELD_VALUE('from'); $orig_from = '' unless defined $orig_from; # 2) keep original From for troubleshooting if (length $orig_from) { &DEFINE_FIELD_FORCED('x-original-from', $orig_from); } # 3) extract display-name robustly (no warnings even if patterns don't match) my $name = ''; if ($orig_from =~ /^\s*"([^"]+)"\s*<[^>]+>/) { $name = $1; } elsif ($orig_from =~ /^\s*([^<]+?)\s*<[^>]+>/) { $name = $1; } elsif ($orig_from =~ /<([^>]+)>/) { my $addr = $1; ($name) = split(/\@/, $addr, 2); } elsif ($orig_from =~ /([^\s\@]+@[^\s\@]+)/) { my $addr = $1; ($name) = split(/\@/, $addr, 2); } # 4) trim; fallback if empty/undef $name = '' unless defined $name; $name =~ s/^\s+|\s+$//g if length $name; $name = 'No Name' unless length $name; # 5) build new From with " via ML:" suffix my $disp = $name . '_via_ML:'; # 6) ensure MAIL_LIST is defined my $ml = defined($MAIL_LIST) ? $MAIL_LIST : ''; # 7) create new From header with quotes my $new_from = $disp . ' <' . $ml . '>'; # 8) set rewritten From only when ML address is available if (length $ml) { &DEFINE_FIELD_FORCED('from', $new_from); } } }; <||
2025/10/04(土)fmlにスクリプトを組み込む方法
メーリングリストfmlのカスタマイズ
さくらインターネットの場合です。多少、UNIX(Linux)の知識が必要なので、尻込みする人がいるかもですが、たいした操作はしないので、手順とおりやれば大丈夫です。
まず該当のメーリングリストの cfファイル をダウンロードして加工します。
cf ファイル の場所は以下のとおりで、ftpアプリ等でダウンロードしましょう。
/home/<アカウント名>/fml/spool/ml/<メーリングリスト名>
cf ファイルに拡張子はついていないので、テキストエディタで無理矢理開きます。
cfファイルの最後のほうに、以下の記述があります。
# YOU CAN EDIT MANUALLY AFTER HERE.この直後に、目的のスクリプトをコピペします。そしてアップロードして元の場所に戻しましょう。
※この際、ファイルの中に、日本語でコメントを残さないのが無難です。
※仮に日本語を使う必要があるのであれば、保存文字コードに気を使ってください。UTF-8が無難でしょう。
その後、SSHクライアント*1でサーバーに接続し、以下のコマンドを順に実行します。
(これが Unixのコマンドです。)
cd fml/spool/ml/<mailing list><mailing list>のところは、メーリングリストの@から左の部分です。
make config.ph
cdは、change directory のコマンドであり、そのディレクトリ(フォルダ)の場所まで移動するコマンドです。
うまく移動できなければ、lsコマンド*2も利用して、cfファイルがある場所まで到着してください。
そのうえで、
make config.phとします。これでfmlにさきほどのスクリプトが組み込まれました。
エラーがでていなければOKです。
動作確認をしてみましょう。
makeしてから反映までしばらく時間がかかることもあるようです。
参考にさせていただいたサイト
さくらインターネットでメーリングリストを開設したら最初にするべきことさくらメーリングリストにおけるreply-to設定 on fml
2025/10/04(土)Gmailにも届く、完璧なメーリングリスト(ML)の作り方(特にビジネス)
メーリングリストは生きている
「ファックス」はいずれなくなるといわれつづけ、しかし一部では現役で使われています。ML(メーリングリスト)も、Slackなどの導入で不要になる、と言われ続け、しかしまだ現役で使われています。
一方で、需要減に対応してか、各レンタルサーバー会社では、MLの機能提供がなおざりになっている状況もみてとっています。
そこでここでは、さくらインターネットを例にとって、メーリングリスをより使いやすくするための技術的tipsをご紹介します。
この記事は、レンタルサーバーでメーリングリストを運用しており、メールが届かない・迷惑メール扱いされるといった問題に悩む技術担当者向けです。
現状のメーリングリストの問題点
運用上の問題点 reply-to
メーリングリスト+Ccで他のアドレス(MLメンバーではないアドレス)がある場合、通常、「全員返信で返信します」。ところが、多くのメーリングリストサービスは、reply-to ヘッダが、MLアドレスまたはFromアドレスの二択(あるいはどちらか一択)の場合がほとんどです。
しかし、実務ではこんな問題が起きます。
あるメール
To: ml<mladress@example.com>Aさん、BさんはMLの登録メンバーではありません。
From: Aさん<a-san@example.com>
Reply-to: ml<mladress@example.com>
Cc: Bさん<b-san@example.com>
これに全員返信すると、
To: ml<mladress@example.com>となり、肝心のAさんが宛先から抜けてしまう!のです。
Cc: Bさん<b-san@example.com>
しかし、返信した人は「全員返信」しているつもりなので、Aさんにも届いていると誤解したままコミュニケーションミスが発生する、ということがビジネスの現場でたまにみられます。
では、常にreply-to を、投稿者(Fromのアドレス)にしてしまうとどうでしょう。
To: ml<mladress@example.com>これに全員返信すると、
From: Aさん<a-san@example.com>
Reply-to: Aさん<a-san@example.com>
Cc: Bさん<b-san@example.com>
Tp: Aさん<a-san@example.com>となり、きちんと全員返信の目的を達成できます。
To: ml<mladress@example.com> ※Ccとなるメーラーもある
Cc: Bさん<b-san@example.com>
ところが、これも問題があります。
仮に、Aさんが登録メンバーだったらどうなるでしょうか。
全員返信すると結果は同じですが
Tp: Aさん<a-san@example.com>Aさんには、同じ内容のメールが2通届きます。
To: ml<mladress@example.com> ※Ccとなるメーラーもある
Cc: Bさん<b-san@example.com>
また、このようなやりとりを他のMLメンバーも参加してお互いに投稿すると、toやcc にMLメンバーのアドレスがどんどん追加されて、宛先欄が汚れるだけではなく、余分なメールが行き交うようになります。
そこで以下のように解決すべきと考えています。
① Fromが、MLメンバーの場合には、reply-to はMLアドレスで問題ない。これで、上記の不具合は解決します。※解決策は下記にて。
② Fromが、MLメンバーではない場合には、reply-to は、FromアドレスとMLアドレスを加える*1。
③ ただし、元のメールにreply-to の設定がある場合には、それのみを生かす。
※google-groups も似たような挙動をします。
受信できない問題
昨今、迷惑メール対策として、いわゆるFrom偽装や、内容改変のあるメール、転送メールに対して、受信サーバーが強硬な姿勢で、メールの受信拒否をしています。Gmailが筆頭で、yahooメールは、通信会社関係も強きの受信拒否をしています。
そうすると、MLメールが受信拒否されたり、あるいは、受信できても迷惑メールに振り分けやすくなります。
ビジネスではメールを受信できていない(+迷惑メールになりやすい)、というのは致命傷ですので、なんとかする必要があります。
そこで、①受信サーバーに受信されやすくする、②受信後、迷惑メールに分類されにくくする、と問題にわけて、それぞれの方策が必要となります。
MLは、いったんMLが運用されているサーバーが受信したうえで、A:必要な加工をして、再度、B:MLサーバーが各登録メンバーに送信する作りになっています。
転送はメールをそのまま“通過”させますが、MLは“再送信”する点が、メール転送とMLの違いです*2。
Aの問題
通常、Subject(件名)を書き換えます。[ml-No:0001]みたいな番号をSubject(件名)の先頭につけます。最近はDKIMという仕組みがほぼ必須となっているのですが、このように重要ヘッダであるSubjectヘッダを書き換えると、DKIM=fail となり、内容改変メールと判定されます。
すると、上記①②の問題が発生しやすくなります。
DKIMが壊れているので、当然にDMARCのDKIMアライメントもfailとなります。
※DKIMアライメントとは、DKIMで署名に使われたドメインとFromのドメインの一致(alignment)を確認することです。
DKIMなどの仕組みは、メールセキュリティ覚書(SPF,DKIM,DMARC)を参照してください。
Bの問題
SPFという正当な送信サーバーから送信されているか、をチェックする仕組みがあります。具体的には、SMTPコマンドMAIL FROM(エンベロープFrom→Return-path:)と、送信サーバーのIPアドレスを取得し、それがMAIL FROMのドメインに設定されているSPF情報と整合性があるかを検証します。
多くのMLサーバーは、自身でMAIL FROMを投稿者ではなく、自らのMLドメインに変更するので、MLドメインでのSPF は pass (成功)します。
正当なサーバーからの送信として解釈されることとなります。
ところが最近は、DMARCというFrom偽装を防ぐ仕組みであり、このMAIL FROM(エンベロープFrom→Return-path:)とヘッダーFromドメインを一致を確認し、それがfailだと、DMARC もfailします。
この場合でも、DKIMが通っていて、DMARCのDKIMアライメントが通っていればいいのですが、上記のとおりDKIMは壊れているので、DMARC も fail します。
すると、このメールはFrom偽装のなりすましメールだということとなり、受信拒否されやすくとなります。
技術的な表現
内容改変によって、DKIMが壊れるので、MLが再度、自身のドメインでDKIM署名しないとDKIMは通りません。ARCによって、一定の信頼チェーンは作れますが、これをどう解釈するかは受信側に委ねられており、完全な対応ではありません(もちろんARCはあったほうがよい)。
すると、DKIMが壊れている以上、DMARCのDKIMアライメント(署名ドメインとFromドメインの一致)も fail します。
同様に、SPFがpassしても、DMARCのSPFアライメントが通りません。
受信できない問題の解決策
現実的な解決策として以下が考えられます。FromをMLアドレスとすることで、DMARCのSPFアライメントを成功させ、DMARC pass とする。DMARCを pass するので、受信確率がぐっと上がります。
ただし、このFromアドレスの変更をするには、ビジネス上の留意点があります。
それは、Fromの表示名、です。
このFromの表示名まで、MLの名前が表示されるようになると、誰からのメールかについてすぐにわからない(本文読まないとわからない)、ということになるのです。
実際の投稿者は、
From: Aさん <a-san@example.com>なのに
From: tips456のML <ml@tips.net>みたいになるのです。これでは困ります(エックスサーバーはこの仕様なのでビジネスで使いづらい。)。
そこで解決策は、
- Fromのアドレスは、MLアドレスであるが
- Fromの表示名は、投稿者の名前
さらに発展形としては、
- Fromのアドレスは、MLアドレスであるが
- Fromの表示名は、"投稿者の名前_via_ML"
Fromヘッダは以下のようになります。
From: "投稿者の名前_via_ML" <ml@tips.net>また技術上は、例えば X-Original-From として
X-Original-From: (投稿者の表示名とメールアドレス)というぐあいに、投稿者の投稿時のFromを残しておくことは有用です。
MLサーバーでDKIMを再署名できないの?
理論上はできると思いますが、さくらインターネットで試みたところ、できませんでした*3。おそらくMLはARC対応で十分と考えているのでしょう。
もし、MLがDKIM再署名しているサーバー等をご存じの方は教えてください。
追記:
google-groups は、MLサーバーで再署名しています。さすがですね。
さくらインターネットではなぜできないのか、くやしいの~。
整理
これまでの議論で、以下のことをすれば、ビジネス上も実務に耐えるメーリングリスト運用ができることとなります。- Fromヘッダについて、アドレスはML、表示名は投稿者+via_MLに変換することで、受信確率を上げて(ほぼ大丈夫)、迷惑メールに分類させる確率を下げる(メールの内容次第)。
- Reply-toは、投稿者がMLメンバーかどうかによって、投稿者のアドレスをいれるかどうか振り分ける
さくらインターネットMLでFromを書き換えてSPFalignmentを成功させる
という記事を投稿しております。これでばっちりです。
ただし、これを メーリングリストプログラムのfmlに組み込むのは、ほんのちょっと、サーバー管理の知識が必要です。しかし、怖がるほどのものではないと思いますので、頑張って設定してみましょう。一度やればあとは簡単です。
どのサービス(レンタルサーバー)を使用するか
私は、さくらインターネットを長く使っており、このレンタルサーバーの議論しかできません*4。最近、縁あってエックスサーバーを触る機会があったのですが、ユーザーの自由度が低く、上記の設定が(たぶん)できないので*5、オススメできません。
追加的施策
不要なヘッダの削除
どうせfailするDKIMヘッダなどをMLサーバーが削除しておければ、受信側のネガティブ要素を無くすことができます。ただし、この措置はかえって偽装メールと判断されかねない要素なので、必須ではないと考えています。
いずれこの点も記載したいと思います。
その他
こちらのサイト https://mekiku.com/view.php?a=89 は、簡潔な方法で解決されています。エレガントでいいですね。これでもいいと思います。ただし、Fromの表示は単に投稿者のFromをそのまま流用しているだけとなります。
この場合、投稿者からの送信かML経由が一目ではわからないので、当サイトでは、_via_ML を付ける方法をご紹介しています。マルチバイトに対応するために、MINEデコード、エンコードもしていますので、スクリプトが長くなっています。
まとめ:ビジネスMLの完璧運用を今すぐ実現
メーリングリスト(ML)は、非同期コミュニケーションの強力なツールですが、DMARC/SPF/DKIMの認証失敗による受信トラブルが最大の敵です。これを解決する鍵は以下の3ステップ:- Fromヘッダの書き換え:MLアドレスをFromに設定し、表示名で「投稿者_via_ML」と明記。SPFアライメントを確保し、配信率を劇的に向上。
- reply-toのスマート設定:投稿者がMLメンバーならMLアドレス、非メンバーならFrom+MLの組み合わせに。Reply Allの混乱を防ぎ、コミュニケーションをスムーズに。
- さくらインターネット活用:DKIM再署名不可の制約をFrom書き換えでカバーし、ARCで信頼性を補強。Xserverのような柔軟性の低いサービスは避けましょう。
これらを実装すれば、ビジネスMLはスパム判定を回避し、確実に届く「完璧なツール」へ。fmlなどのMLソフトに組み込む際は、テスト運用からスタートを。あなたのチームの生産性を高める一手、ぜひ試してみてください!
2025/09/22(月)必ず受信できるメーリングリストの運用方法(From書換)
メーリングリストの受信トラブルを解決する
メーリングリスト運用、特にsubjectを変更するMLは、「受信できない!」「迷惑メールに入りすぎる!」などのトラブルが発生しがちです。ML(メーリングリスト)の受信確率を上げるには、以下の運用が一番です。
後半では、メーリングリストでの返信先問題(reply-to)も取り上げます。
SPFを活用する方式
(ML側)1.SPF設定---------必須
2.ヘッダFROM書換--必須(MLのドメインに書き換える)
3. reply-to:に投稿者とMLの両方をいれるようにする。
これで、SPFはPassします。
4.DMARC設定-------強い推奨(ただし当面はp=none設定)
5.ARC導入---------推奨
MLドメインのDMARCを rejectに設定したとしても、Fromが書き換わっているので、DMARCアライメントもpassしますが、運用当初はnoneで様子をみたほうがよいと思います。
※DMARCアライメントとは、ヘッダFromとSPFで認証されたreturn-path(=最終的に有効なMAIL FROM/envelope-from)が一致していること。
仮に、投稿者のDMARCがrejctに設定されていたとしても、FromがMLドメインに書き換わっているので、DMARCアライメントは関係がなくなります。
DKIMを活用する方式
(ML側)1. DKIM設定---------必須(MLが配送するメールにDKIMが必要です。)
2. +ヘッダFROM書換--必須(MLのドメインに書き換える)
3. reply-to:に投稿者とMLの両方をいれるようにする。
これでDKIMはpassします。
4. DMARC設定--------強い推奨(ただし当面はp=none設定)
5.ARC導入----------推奨
MLドメインのDMARCを rejectに設定したとしても、Fromが書き換わっているので、DMARCアライメントもpassしますが、運用当初はnoneで様子をみたほうがよいと思います。
仮に、投稿者のDMARCがrejctに設定されていたとしても、FromがMLドメインに書き換わっているので、DMARCアライメントは関係がなくなります。
ARCについて
ARCは、メーリングリスト*1が投稿者からのメールを受信した際のSPF,DKIMの認証情報を、次のサーバー(MTA)に引き継ぐもので、ヘッダに記載されます。しかし、ARCは有効な仕組みではありますが、完璧ではなく、受信側サーバーが斟酌してくれるのを祈るしかありません。
その点、ヘッダFROM書換は、MLの場合、投稿者からではなく、FROMからの投稿ということになるので、エラーメールとなる確率がぐっと下がります。
さらにDMARCを設定している場合には、DMARCアライメントも通るので、さらにエラーメールとなる確率がぐっと下がります。
さくらインターネットMLでFromを書き換えてSPFalignmentを成功させる・メールセキュリティ覚書(SPF,DKIM,DMARC)を参照。
ヘッダFrom書換による影響
上記のとおり、MLの運用においては、From書換が最強です。ヘッダFrom書換のデメリットとして、メール受信者のユーザーエクスペリエンスが低下する(Fromヘッダが投稿者ではなくMLドメインとなる)ということが度々指摘されます。
From: taro_nippon@example.jp → mlname@mldomain.jp となるということです。
しかし、これもメールアドレス部分ではなく、「表示名」部分を工夫することで、一定の対応が可能です。
From: 日本太郎<taro_nippon@example.jp> → 日本太郎 via ML<mlname@mldomain.jp> や
From: taro_nippon@example.jp → taro_nippon via ML<mlname@mldomain.jp>
となるということです。※表示名は他にも工夫の余地があります。
MLとはそういうものだ、という認識さえ登録メンバーに意識付けすれば*2、特に混乱はないように考えています。
MLへの返信が、適切に実施されるための要件
返信時の挙動
一般的なMLでは、reply-toヘッダはMLアドレスが指定されることが多いです(fmlのデフォルトはMLアドレス、Mailmanのデフォルトはreply-toなし)。すると、以下の問題が生じます。
ML(メーリングリスト)メンバーではない外部アドレスからの投稿の場合、reply-toがMLアドレスだけとなり、そのメールに返信(全員返信含む)すると、MLにしか返信されないので、当該外部の投稿者に返信されない。これはFrom書換しなくても生じる問題ですが、From書換だけをしても残る問題です。
よって、MLは、reply-to の設定に関し、以下の挙動が望まれるところです(下記A,Bの両方)。
A. MK(メーリングリスト)外部からの投稿に関しては、 reply-to に、①MLのメールアドレスのほか、②当該外部投稿者のメールアドレスの2つをセットする。Aは、外部投稿者への返信が漏れてしまうことを防ぎます。
B. MK内部(MLの登録メンバー)からの投稿に関しては、reply-to に、①MLのメールアドレスのみをセットする。
Aを内部投稿者にも貫くと、reply-toや宛先にMLメンバーのアドレスが次々と継ぎ足されることとなり、メールの宛先欄が混乱することとなるのでBが必要となります。
この点を改善しようとしたのが、メーリングリスト改善/さくらインターネット/Reply-toヘッダの記事です。
もうひとつのバリュエーションも上記の記事に加えました。
メーリングリストメンバーからの投稿については、reply-toの設定が元メールにあればそれを生かし、なければreply-toに、MLアドレスを設定する。
メンバー以外の外部からの投稿については、reply-toの設定があればそのまま生かし、reply-to設定がなければ、当該外部の方(Fromヘッダにあるアドレス)をreply-toに設定する。
最強のメーリングリスト運用方法
- ヘッダFrom書換
- Reply-toヘッダの適切な処理
これを実務記事にしたのが、さくらインターネットMLでFromを書き換えてSPFalignmentを成功させるの記事です。
ML登録者が気を付けること
- MLからのメールを単純転送して、他のドメイン(例:Gmail)で受け取ってはいけません。せっかくのSPFが崩れます。管理者に最終的に受け取るアドレスを登録してもらいましょう。
- Gmailに転送したいなら、単純転送ではなく、POP/IMAPアクセスにしましょう。
2025/09/20(土)さくらインターネットMLでFromを書き換えてSPFalignmentを成功させる
このページの簡便法は、間便法:Gmailにも届くメーリングリスト-さくらインターネット をご参照ください。
さくらインターネットのメーリングリストでFromを書き換える
メーリングリスト改善/さくらインターネット/Reply-toヘッダにて、reply-toの問題は解決しました。続いて、DMARCを通させるためにFrom書換をしてみましょう。
google groups,Microsoft365などは、From書換をしています。さくらインターネットもコントロールパネルで変更できるようにしてほしいものですが、執筆時点では対応していません。
Fromを書き換えないことには、DMARCのアライメントが通りません(SPFアライメント(alignment)は、ヘッダFROMとMLの発信envelope-from(return-path)の一致をみます)。
ここでは、From 投稿者の表示名_via_ML <MLアドレス> と変更する方法を紹介します。これで SPFアライメント は通ることとなります(ヘッダFromとenvelope-from(return-path)が一致)。
もし、投稿者の表示名が存在しない場合(=メールアドレスのみの場合)は、投稿者のアドレスの@より左の部分だけを表示名として取り込みます。
DKIMはMLが件名等を書き換えるため、壊れたままとなるので、DKIMアライメントに行き着く前に失敗します。しかし、Gmail は、SPFアライメントか,DKIMアライメントが通れば受信してくれるので、大丈夫です(執筆時点)。
※さくらインターネットは、MLにおいてDKIMを再署名する機能は提供してない。
設定の仕方は、fmlにスクリプトを組み込む方法 をご参照ください。
SUFFIXにつける追加文字(SUFFIX)は自由に変更できます。日本語もOK。
スクリプトを作成するファイル(CF)の文字コードは必ず確認してください。
my $DISPLAY_NAME_SUFFIX = '_via_ML';出力は、UTF-8 でのMIMEエンコーディングをデフォルトとしていますが、ISO_2022_JPでエンコードしたい場合には、下記のように変更してください。
my $CF_FILE_ENCODING = 'shiftjis'; # Change this if CF file uses different encoding
my $OUTPUT_MIME_ENCODING = 'ISO-2022-JP';以下スクリプト本体
$START_HOOK = q{ # ======================================== # Configuration Section # ======================================== # Suffix to append to display names (configurable) # Change this value to customize the suffix my $DISPLAY_NAME_SUFFIX = '_via_ML'; # Character encoding of this CF file (for Japanese suffix support) # Options: 'utf-8', 'shiftjis', 'euc-jp', 'iso-2022-jp' my $CF_FILE_ENCODING = 'shiftjis'; # Change this if CF file uses different encoding # Output MIME encoding for From header # Options: 'UTF-8' (international standard), 'ISO-2022-JP' (Japanese traditional) my $OUTPUT_MIME_ENCODING = 'UTF-8'; # Change to 'ISO-2022-JP' for legacy systems # Convert suffix to UTF-8 if it contains non-ASCII characters if ($DISPLAY_NAME_SUFFIX =~ /[\x80-\xFF]/) { # Suffix contains non-ASCII, decode from CF file encoding if ($CF_FILE_ENCODING ne 'utf-8') { my $temp = decode($CF_FILE_ENCODING, $DISPLAY_NAME_SUFFIX); if (defined $temp) { $DISPLAY_NAME_SUFFIX = $temp; } } } # ======================================== # Reply-To Header Processing # ======================================== # Check if sender is a mailing list member if (&MailListMemberP($From_address)) { # Member: Set Reply-To to mailing list if not already set unless (&GET_HEADER_FIELD_VALUE('reply-to')) { &DEFINE_FIELD_FORCED("reply-to", $MAIL_LIST); } } else { # Non-member: Add both original address and ML to Reply-To if (&GET_HEADER_FIELD_VALUE('reply-to')) { # Append ML to existing Reply-To &DEFINE_FIELD_FORCED("reply-to", &GET_HEADER_FIELD_VALUE('reply-to') . "," . $MAIL_LIST); } else { # Create new Reply-To with sender and ML &DEFINE_FIELD_FORCED("reply-to", $From_address . "," . $MAIL_LIST); } } # ======================================== # From Header Rewrite Processing # ======================================== # Load required modules use Encode qw(decode encode); use Encode::MIME::Header; # Get and save original From header my $orig_from = &GET_HEADER_FIELD_VALUE('from'); &DEFINE_FIELD_FORCED('x-original-from', $orig_from); # Extract display name and email address from From header my ($display_name, $email_address) = extract_from_parts($orig_from); # Build new From header my $new_from; if ($display_name) { # Add configured suffix to display name $display_name .= $DISPLAY_NAME_SUFFIX; # Check if string contains non-ASCII characters if ($display_name =~ /[^\x00-\x7F]/) { # Ensure proper UTF-8 internal representation utf8::decode($display_name) unless utf8::is_utf8($display_name); # Encode entire display name as single MIME-Header block # This creates a proper RFC 2047 encoded-word my $encoded_name; if ($OUTPUT_MIME_ENCODING eq 'UTF-8') { # Use UTF-8 encoding (modern standard) $encoded_name = encode('MIME-Header', $display_name); } else { # Use ISO-2022-JP encoding (Japanese traditional) $encoded_name = encode('MIME-Header-ISO_2022_JP', $display_name); } $new_from = $encoded_name . ' <' . $MAIL_LIST . '>'; } else { # ASCII only, use with quotes $display_name =~ s/"/\\"/g; # Escape existing quotes $new_from = '"' . $display_name . '" <' . $MAIL_LIST . '>'; } } else { # If no display name found, use local part of email address my $local_part = $email_address || 'unknown'; $local_part =~ s/@.*$//; $new_from = '"' . $local_part . $DISPLAY_NAME_SUFFIX . '" <' . $MAIL_LIST . '>'; } # Set new From header &DEFINE_FIELD_FORCED('from', $new_from); # ======================================== # Function: Parse From Header # ======================================== sub extract_from_parts { my ($from_header) = @_; my $display_name = ''; my $email_address = ''; # Return empty if From header is empty return ('', '') unless $from_header; # Store original for processing my $decoded_from = $from_header; # Check if header contains MIME encoding if ($from_header =~ /=\?[\w\-]+\?[BQbq]\?/) { # MIME encoded header detected - decode it my $temp = decode('MIME-Header', $from_header); if (defined $temp) { $decoded_from = $temp; # Ensure UTF-8 flag is set properly utf8::decode($decoded_from) unless utf8::is_utf8($decoded_from); } } else { # Not MIME encoded - should be ASCII only per RFC 5322 # Log warning if non-ASCII bytes detected (RFC violation) if ($decoded_from =~ /[\x80-\xFF]/) { &LOG("warning: Non-ASCII bytes in non-MIME header (RFC violation): $from_header"); # Special case: ISO-2022-JP with escape sequences # Some older systems might pass this through if ($decoded_from =~ /\x1b\$B/) { my $temp = decode('iso-2022-jp', $decoded_from); if (defined $temp) { $decoded_from = $temp; } } # For other non-ASCII cases, continue with original # (will likely result in using email local part as fallback) } } # Parse From header structure # Remove any leading/trailing whitespace $decoded_from =~ s/^\s+//; $decoded_from =~ s/\s+$//; # Pattern 1: "Display Name" <email@example.com> if ($decoded_from =~ /^"([^"]*(?:\\.[^"]*)*)"\s*<([^>]+)>/) { $display_name = $1; $email_address = $2; # Unescape quotes in display name $display_name =~ s/\\"/"/g; } # Pattern 2: Display Name <email@example.com> (no quotes) elsif ($decoded_from =~ /^([^<]+?)\s*<([^>]+)>/) { $display_name = $1; $email_address = $2; # Clean up display name $display_name =~ s/^\s+//; $display_name =~ s/\s+$//; } # Pattern 3: <email@example.com> (no display name) elsif ($decoded_from =~ /<([^>]+)>/) { $email_address = $1; $display_name = ''; } # Pattern 4: email@example.com (bare email) elsif ($decoded_from =~ /([^\s<>]+@[^\s<>]+)/) { $email_address = $1; $display_name = ''; } else { # Unable to parse &LOG("warning: Unable to parse From header: $decoded_from"); # Try to extract email as last resort if ($decoded_from =~ /([^\s<>]+@[^\s<>]+)/) { $email_address = $1; } } # Clean up extracted values $email_address =~ s/^\s+//; $email_address =~ s/\s+$//; if ($display_name) { # Remove extra whitespace $display_name =~ s/^\s+//; $display_name =~ s/\s+$//; $display_name =~ s/\s+/ /g; # IMPORTANT: Remove surrounding quotes that may have been included from MIME decode # This prevents double-quoting when re-encoding $display_name =~ s/^"//; $display_name =~ s/"$//; } return ($display_name, $email_address); } # ======================================== # End of Hook Processing # ======================================== };