問い合わせがあった際、管理者と利用者に別々でメールを送信する機能を実装した。
本来であれば管理者と利用者にメールが一件ずつ送信されるが、管理者にのみ送信されるはずのメールが利用者にも送信されてしまった。今回はその解決方法についてのメモ書き。
結論としては、
が原因だった。
以下のコードは問題があった部分(一部省略)。他の実装方法もあるが、今回はこれでいく。
<?php
# ...省略
class CustomMailer extends PHPMailer {
# ...省略
public function sendEmail($address, $subject, $body){
# ...省略
parent::addAddress($address);
$this->Subject = $subject;
$this->Body = $body;
parent::send();
}
}
# ...省略
$mailer = new CustomMailer;
$mailer->sendEmail('admin@example.com', '件名1', '本文1');
$mailer->sendEmail('user@example.com', '件名2', '本文2');
最初に目をつけたのは、mailer.phpの$mailer->sendEmail()
。
まずはparent::addAddress()
の中身を確認することにした。
/**
* Add a "To" address.
*
* @param string $address The email address to send to
* @param string $name
*
* @throws Exception
*
* @return bool true on success, false if address already used or invalid in some way
*/
public function addAddress($address, $name = '')
{
return $this->addOrEnqueueAnAddress('to', $address, $name);
}
$this->addOrEnqueueAnAddress()
を呼び出しているようだ。中身を確認する。
記事を書いている最中に知ったが、関数に含まれているEnqueue
はキューの末尾に要素を追加するという意味を持つらしい。「末尾に追加する」とある時点で気づくべきだった。
/**
* Add an address to one of the recipient arrays or to the ReplyTo array. Because PHPMailer
* can't validate addresses with an IDN without knowing the PHPMailer::$CharSet (that can still
* be modified after calling this function), addition of such addresses is delayed until send().
* Addresses that have been added already return false, but do not throw exceptions.
*
* @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo'
* @param string $address The email address
* @param string $name An optional username associated with the address
*
* @throws Exception
*
* @return bool true on success, false if address already used or invalid in some way
*/
protected function addOrEnqueueAnAddress($kind, $address, $name)
{
$pos = false;
if ($address !== null) {
$address = trim($address);
$pos = strrpos($address, '@');
}
if (false === $pos) {
//At-sign is missing.
$error_message = sprintf(
'%s (%s): %s',
$this->lang('invalid_address'),
$kind,
$address
);
$this->setError($error_message);
$this->edebug($error_message);
if ($this->exceptions) {
throw new Exception($error_message);
}
return false;
}
if ($name !== null && is_string($name)) {
$name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
} else {
$name = '';
}
$params = [$kind, $address, $name];
//Enqueue addresses with IDN until we know the PHPMailer::$CharSet.
//Domain is assumed to be whatever is after the last @ symbol in the address
if (static::idnSupported() && $this->has8bitChars(substr($address, ++$pos))) {
if ('Reply-To' !== $kind) {
if (!array_key_exists($address, $this->RecipientsQueue)) {
$this->RecipientsQueue[$address] = $params;
return true;
}
} elseif (!array_key_exists($address, $this->ReplyToQueue)) {
$this->ReplyToQueue[$address] = $params;
return true;
}
return false;
}
//Immediately add standard addresses without IDN.
return call_user_func_array([$this, 'addAnAddress'], $params);
}
例外が発生しておらず、対象のアドレスがIDNの場合は$this->RecipientsQueue
か$this->ReplyToQueue
に情報(種類、アドレス、名前)が追加され、IDNでない場合はaddAnAddress()
関数を呼び出すようになっている。
/**
* Add an address to one of the recipient arrays or to the ReplyTo array.
* Addresses that have been added already return false, but do not throw exceptions.
*
* @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo'
* @param string $address The email address to send, resp. to reply to
* @param string $name
*
* @throws Exception
*
* @return bool true on success, false if address already used or invalid in some way
*/
protected function addAnAddress($kind, $address, $name = '')
{
if (!in_array($kind, ['to', 'cc', 'bcc', 'Reply-To'])) {
$error_message = sprintf(
'%s: %s',
$this->lang('Invalid recipient kind'),
$kind
);
$this->setError($error_message);
$this->edebug($error_message);
if ($this->exceptions) {
throw new Exception($error_message);
}
return false;
}
if (!static::validateAddress($address)) {
$error_message = sprintf(
'%s (%s): %s',
$this->lang('invalid_address'),
$kind,
$address
);
$this->setError($error_message);
$this->edebug($error_message);
if ($this->exceptions) {
throw new Exception($error_message);
}
return false;
}
if ('Reply-To' !== $kind) {
if (!array_key_exists(strtolower($address), $this->all_recipients)) {
$this->{$kind}[] = [$address, $name];
$this->all_recipients[strtolower($address)] = true;
return true;
}
} elseif (!array_key_exists(strtolower($address), $this->ReplyTo)) {
$this->ReplyTo[strtolower($address)] = [$address, $name];
return true;
}
return false;
}
addAnAddress()
内で例外がない場合、対象の情報は$this->{$kind}[] = [$address, $name];
の部分で相応しいプロパティに追加される。送信先の場合はこの部分で$this->to[]
に情報が入れられるようだ。
やっと両者にメールが二重で送信される理由が分かった。
$this->to
はてっきり一つの情報しか保持しないと思っていたが、配列で複数のアドレス情報を管理する。そのためparent::addAddress()
を実行するたびに$this->to
に情報が溜まってしまい結果、parent::send()
を実行する度$this->to
が保持しているアドレス全てにメールを送信してしまっているのだ(多分)。
PHPMailer内には送信メールの受信情報?をすべて削除する関数が存在するのでそちらを使ってやれば良い。
/**
* Clear all recipient types.
*/
public function clearAllRecipients()
{
$this->to = [];
$this->cc = [];
$this->bcc = [];
$this->all_recipients = [];
$this->RecipientsQueue = [];
}
ccやbccを削除したくない場合は下の関数を利用する。
/**
* Clear all To recipients.
*/
public function clearAddresses()
{
foreach ($this->to as $to) {
unset($this->all_recipients[strtolower($to[0])]);
}
$this->to = [];
$this->clearQueuedAddresses('to');
}
この関数を組み込んだことで今回の問題が解決した。
<?php
# ...省略
class CustomMailer extends PHPMailer {
# ...省略
public function sendEmail($address, $subject, $body){
# ...省略
parent::addAddress($address);
$this->Subject = $subject;
$this->Body = $body;
parent::send();
parent::clearAllRecipients();
}
}
間違えている部分がある場合はご指摘いただけると幸いです。