Skip to content

Commit f56d245

Browse files
authored
Merge pull request #1316 from EC-CUBE/feature/issue-819-mailmaga-one-click-unsubscribe
Issue #819: RFC 8058対応のワンクリックメルマガ登録解除機能
2 parents 16d56cd + 84a0607 commit f56d245

10 files changed

Lines changed: 901 additions & 6 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<!--{*
2+
* This file is part of EC-CUBE
3+
*
4+
* Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
5+
*
6+
* http://www.ec-cube.co.jp/
7+
*
8+
* This program is free software; you can redistribute it and/or
9+
* modify it under the terms of the GNU General Public License
10+
* as published by the Free Software Foundation; either version 2
11+
* of the License, or (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* GNU General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU General Public License
19+
* along with this program; if not, write to the Free Software
20+
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
21+
*}-->
22+
23+
<div id="undercolumn">
24+
<h2 class="title"><!--{$tpl_title|h}--></h2>
25+
<div id="undercolumn_mailmaga_unsubscribe">
26+
<div id="complete_area">
27+
<!--{if $tpl_success}-->
28+
<p class="message success"><!--{$tpl_message|h}--></p>
29+
<p>今後、メールマガジンは配信されません。</p>
30+
<div class="btn_area">
31+
<ul>
32+
<li>
33+
<a href="<!--{$smarty.const.TOP_URL}-->"><img class="hover_change_image" src="<!--{$TPL_URLPATH}-->img/button/btn_toppage.jpg" alt="トップページへ" /></a>
34+
</li>
35+
</ul>
36+
</div>
37+
<!--{elseif $tpl_message}-->
38+
<p class="message error"><!--{$tpl_message|h}--></p>
39+
<div class="btn_area">
40+
<ul>
41+
<li>
42+
<a href="<!--{$smarty.const.TOP_URL}-->"><img class="hover_change_image" src="<!--{$TPL_URLPATH}-->img/button/btn_toppage.jpg" alt="トップページへ" /></a>
43+
</li>
44+
</ul>
45+
</div>
46+
<!--{else}-->
47+
<p class="message">メールアドレス: <strong><!--{$tpl_email|h}--></strong></p>
48+
<p>メールマガジンの登録を解除しますか?</p>
49+
<form method="post">
50+
<input type="hidden" name="mode" value="confirm" />
51+
<div class="btn_area">
52+
<ul>
53+
<li>
54+
<button type="submit" class="btn">登録を解除する</button>
55+
</li>
56+
</ul>
57+
</div>
58+
</form>
59+
<!--{/if}-->
60+
</div>
61+
</div>
62+
</div>

data/class/SC_SendMail.php

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ class SC_SendMail
4444
public $from;
4545
/** @var string */
4646
public $reply_to;
47+
/** @var array */
48+
protected $customHeaders;
4749

4850
/**
4951
* コンストラクタ
@@ -58,8 +60,9 @@ public function __construct()
5860
$this->body = '';
5961
$this->cc = '';
6062
$this->bcc = '';
61-
$this->replay_to = '';
63+
$this->reply_to = '';
6264
$this->return_path = '';
65+
$this->customHeaders = [];
6366
$this->backend = MAIL_BACKEND;
6467
$this->host = SMTP_HOST;
6568
$this->port = SMTP_PORT;
@@ -144,6 +147,47 @@ public function setReturnPath($return_path)
144147
$this->return_path = $return_path;
145148
}
146149

150+
/**
151+
* カスタムヘッダーを追加
152+
*
153+
* @param string $name ヘッダー名
154+
* @param string $value ヘッダー値
155+
*/
156+
public function addCustomHeader($name, $value)
157+
{
158+
// ヘッダー名の形式チェック(RFC 7230 token)
159+
if (!is_string($name) || $name === '' || !preg_match('/^[!#$%&\'*+\-.^_`|~0-9A-Za-z]+$/', $name)) {
160+
trigger_error('ヘッダー名の形式が不正です。', E_USER_WARNING);
161+
162+
return;
163+
}
164+
165+
// ヘッダーインジェクション対策
166+
if (preg_match('/[\r\n]/', $name) || preg_match('/[\r\n]/', $value)) {
167+
trigger_error('ヘッダーに改行文字は使用できません。', E_USER_WARNING);
168+
169+
return;
170+
}
171+
172+
// 重要なヘッダーの上書きを防止
173+
$protectedHeaders = ['from', 'to', 'subject', 'cc', 'bcc', 'reply-to', 'return-path', 'date', 'mime-version', 'content-type', 'content-transfer-encoding'];
174+
if (in_array(strtolower($name), $protectedHeaders, true)) {
175+
trigger_error('保護されたヘッダーは上書きできません: '.$name, E_USER_WARNING);
176+
177+
return;
178+
}
179+
180+
$this->customHeaders[$name] = $value;
181+
}
182+
183+
/**
184+
* カスタムヘッダーをクリア
185+
*/
186+
public function clearCustomHeaders()
187+
{
188+
$this->customHeaders = [];
189+
}
190+
147191
// 件名の設定
148192
public function setSubject($subject)
149193
{
@@ -284,6 +328,11 @@ public function getBaseHeader()
284328
$arrHeader['Date'] = date('D, j M Y H:i:s O');
285329
$arrHeader['Content-Transfer-Encoding'] = '7bit';
286330

331+
// カスタムヘッダーをマージ
332+
foreach ($this->customHeaders as $name => $value) {
333+
$arrHeader[$name] = $value;
334+
}
335+
287336
return $arrHeader;
288337
}
289338

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<?php
2+
/*
3+
* This file is part of EC-CUBE
4+
*
5+
* Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
6+
*
7+
* http://www.ec-cube.co.jp/
8+
*
9+
* This program is free software; you can redistribute it and/or
10+
* modify it under the terms of the GNU General Public License
11+
* as published by the Free Software Foundation; either version 2
12+
* of the License, or (at your option) any later version.
13+
*
14+
* This program is distributed in the hope that it will be useful,
15+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
* GNU General Public License for more details.
18+
*
19+
* You should have received a copy of the GNU General Public License
20+
* along with this program; if not, write to the Free Software
21+
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
22+
*/
23+
24+
/**
25+
* メルマガ登録解除トークンクリーンアップバッチクラス
26+
*
27+
* Issue #819: メルマガワンクリック登録解除
28+
* 期限切れ・使用済みのトークンを定期的にクリーンアップします。
29+
*
30+
* @author EC-CUBE CO.,LTD.
31+
*
32+
* @version $Id$
33+
*/
34+
class SC_Batch_CleanupMailmagaToken extends SC_Batch
35+
{
36+
/**
37+
* バッチ処理を実行する
38+
*
39+
* 期限切れまたは使用済みのメルマガ登録解除トークンを削除します。
40+
*
41+
* 推奨実行頻度: 1日1回(深夜のメンテナンス時間帯)
42+
*
43+
* @param array $args コマンドライン引数(未使用)
44+
*
45+
* @return void
46+
*/
47+
public function execute($args = [])
48+
{
49+
$this->start('メルマガ登録解除トークンクリーンアップバッチ');
50+
51+
try {
52+
$count = SC_Helper_Mailmaga_Ex::cleanupExpiredTokens();
53+
54+
if ($count > 0) {
55+
$msg = "期限切れ・使用済みトークン {$count} 件を削除しました。";
56+
$this->printLog($msg);
57+
} else {
58+
$msg = 'クリーンアップ対象のトークンはありませんでした。';
59+
$this->printLog($msg);
60+
}
61+
62+
$this->end();
63+
} catch (Exception $e) {
64+
$msg = 'エラーが発生しました: '.$e->getMessage();
65+
$this->printLog($msg);
66+
GC_Utils_Ex::gfPrintLog($msg, ERROR_LOG_REALFILE);
67+
$this->end(true);
68+
}
69+
}
70+
71+
/**
72+
* バッチ開始ログを出力する
73+
*
74+
* @param string $message バッチ名
75+
*
76+
* @return void
77+
*/
78+
protected function start($message = '')
79+
{
80+
$msg = '========================================';
81+
$this->printLog($msg);
82+
$msg = $message.' 開始: '.date('Y-m-d H:i:s');
83+
$this->printLog($msg);
84+
}
85+
86+
/**
87+
* バッチ終了ログを出力する
88+
*
89+
* @param bool $error エラー終了フラグ
90+
*
91+
* @return void
92+
*/
93+
protected function end($error = false)
94+
{
95+
if ($error) {
96+
$msg = 'バッチ処理がエラーで終了しました: '.date('Y-m-d H:i:s');
97+
} else {
98+
$msg = 'バッチ処理が正常に終了しました: '.date('Y-m-d H:i:s');
99+
}
100+
$this->printLog($msg);
101+
$msg = '========================================';
102+
$this->printLog($msg);
103+
}
104+
105+
/**
106+
* ログメッセージを出力する
107+
*
108+
* @param string $message ログメッセージ
109+
*
110+
* @return void
111+
*/
112+
protected function printLog($message)
113+
{
114+
GC_Utils_Ex::gfPrintLog($message, LOG_REALFILE);
115+
echo $message.PHP_EOL;
116+
}
117+
}

data/class/helper/SC_Helper_Mail.php

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,16 @@ public static function sfSendMailmagazine($send_id)
531531
$subjectBody = preg_replace('/{name}/', $customerName, $arrMail['subject']);
532532
$mailBody = preg_replace('/{name}/', $customerName, $arrMail['body']);
533533

534+
// ワンクリック登録解除トークンの生成
535+
$token = SC_Helper_Mailmaga_Ex::generateUnsubscribeToken(
536+
$arrDestination['customer_id'],
537+
$send_id,
538+
$arrDestination['email']
539+
);
540+
541+
// ワンクリック登録解除URLの生成
542+
$unsubscribeUrl = SC_Helper_Mailmaga_Ex::getUnsubscribeUrl($token);
543+
534544
$objMail->setItem(
535545
$arrDestination['email'],
536546
$subjectBody,
@@ -542,16 +552,23 @@ public static function sfSendMailmagazine($send_id)
542552
$objSite['email04'] // errors_to
543553
);
544554

555+
// RFC 8058 ヘッダーの追加
556+
$objMail->addCustomHeader('List-Unsubscribe', '<'.$unsubscribeUrl.'>');
557+
$objMail->addCustomHeader('List-Unsubscribe-Post', 'List-Unsubscribe=One-Click');
558+
545559
// テキストメール配信の場合
546560
if ($arrMail['mail_method'] == 2) {
547-
$sendResut = $objMail->sendMail();
561+
$sendResult = $objMail->sendMail();
548562
// HTMLメール配信の場合
549563
} else {
550-
$sendResut = $objMail->sendHtmlMail();
564+
$sendResult = $objMail->sendHtmlMail();
551565
}
552566

567+
// カスタムヘッダーをクリア(次の送信のため)
568+
$objMail->clearCustomHeaders();
569+
553570
// 送信完了なら1、失敗なら2をメール送信結果フラグとしてDBに挿入
554-
if (!$sendResut) {
571+
if (!$sendResult) {
555572
$sendFlag = '2';
556573
} else {
557574
// 完了を 1 増やす
@@ -584,10 +601,10 @@ public static function sfSendMailmagazine($send_id)
584601

585602
// テキストメール配信の場合
586603
if ($arrMail['mail_method'] == 2) {
587-
$sendResut = $objMail->sendMail();
604+
$sendResult = $objMail->sendMail();
588605
// HTMLメール配信の場合
589606
} else {
590-
$sendResut = $objMail->sendHtmlMail();
607+
$sendResult = $objMail->sendHtmlMail();
591608
}
592609

593610
return;

0 commit comments

Comments
 (0)