1<?php
2
3/**
4 * Handles batch mailing with Swift Mailer with fail-safe support.
5 * Restarts the connection if it dies and then continues where it left off.
6 * Please read the LICENSE file
7 * @copyright Chris Corbyn <chris@w3style.co.uk>
8 * @author Chris Corbyn <chris@w3style.co.uk>
9 * @package Swift
10 * @license GNU Lesser General Public License
11 */
12class Swift_BatchMailer
13{
14  /**
15   * The current instance of Swift.
16   * @var Swift
17   */
18  protected $swift;
19  /**
20   * The maximum number of times a single recipient can be attempted before giving up.
21   * @var int
22   */
23  protected $maxTries = 2;
24  /**
25   * The number of seconds to sleep for if an error occurs.
26   * @var int
27   */
28  protected $sleepTime = 0;
29  /**
30   * Failed recipients (undeliverable)
31   * @var array
32   */
33  protected $failed = array();
34  /**
35   * The maximum number of successive failures before giving up.
36   * @var int
37   */
38  protected $maxFails = 0;
39  /**
40   * A temporary copy of some message headers.
41   * @var array
42   */
43  protected $headers = array();
44
45  /**
46   * Constructor.
47   * @param Swift The current instance of Swift
48   */
49  public function __construct(Swift $swift)
50  {
51    $this->setSwift($swift);
52  }
53  /**
54   * Set the current Swift instance.
55   * @param Swift The instance
56   */
57  public function setSwift(Swift $swift)
58  {
59    $this->swift = $swift;
60  }
61  /**
62   * Get the Swift instance which is running.
63   * @return Swift
64   */
65  public function getSwift()
66  {
67    return $this->swift;
68  }
69  /**
70   * Set the maximum number of times a single address is allowed to be retried.
71   * @param int The maximum number of tries.
72   */
73  public function setMaxTries($max)
74  {
75    $this->maxTries = abs($max);
76  }
77  /**
78   * Get the number of times a single address will be attempted in a batch.
79   * @return int
80   */
81  public function getMaxTries()
82  {
83    return $this->maxTries;
84  }
85  /**
86   * Set the amount of time to sleep for if an error occurs.
87   * @param int Number of seconds
88   */
89  public function setSleepTime($secs)
90  {
91    $this->sleepTime = abs($secs);
92  }
93  /**
94   * Get the amount of time to sleep for on errors.
95   * @return int
96   */
97  public function getSleepTime()
98  {
99    return $this->sleepTime;
100  }
101  /**
102   * Log a failed recipient.
103   * @param string The email address.
104   */
105  public function addFailedRecipient($address)
106  {
107    $this->failed[] = $address;
108    $this->failed = array_unique($this->failed);
109  }
110  /**
111   * Get all recipients which failed in this batch.
112   * @return array
113   */
114  public function getFailedRecipients()
115  {
116    return $this->failed;
117  }
118  /**
119   * Clear out the list of failed recipients.
120   */
121  public function flushFailedRecipients()
122  {
123    $this->failed = null;
124    $this->failed = array();
125  }
126  /**
127   * Set the maximum number of times an error can be thrown in succession and still be hidden.
128   * @param int
129   */
130  public function setMaxSuccessiveFailures($fails)
131  {
132    $this->maxFails = abs($fails);
133  }
134  /**
135   * Get the maximum number of times an error can be thrown and still be hidden.
136   * @return int
137   */
138  public function getMaxSuccessiveFailures()
139  {
140    return $this->maxFails;
141  }
142  /**
143   * Restarts Swift forcibly.
144   */
145  protected function forceRestartSwift()
146  {
147    //Pre-empting problems trying to issue "QUIT" to a dead connection
148    $this->swift->connection->stop();
149    $this->swift->connection->start();
150    $this->swift->disconnect();
151    //Restart swift
152    $this->swift->connect();
153  }
154  /**
155   * Takes a temporary copy of original message headers in case an error occurs and they need restoring.
156   * @param Swift_Message The message object
157   */
158  protected function copyMessageHeaders(&$message)
159  {
160    $this->headers["To"] = $message->headers->has("To") ?
161      $message->headers->get("To") : null;
162    $this->headers["Reply-To"] = $message->headers->has("Reply-To") ?
163      $message->headers->get("Reply-To") : null;
164    $this->headers["Return-Path"] = $message->headers->has("Return-Path") ?
165      $message->headers->get("Return-Path") : null;
166    $this->headers["From"] = $message->headers->has("From") ?
167      $message->headers->get("From") : null;
168  }
169  /**
170   * Restore message headers to original values in the event of a failure.
171   * @param Swift_Message The message
172   */
173  protected function restoreMessageHeaders(&$message)
174  {
175    foreach ($this->headers as $name => $value)
176    {
177      $message->headers->set($name, $value);
178    }
179  }
180  /**
181   * Run a batch send in a fail-safe manner.
182   * This operates as Swift::batchSend() except it deals with errors itself.
183   * @param Swift_Message To send
184   * @param Swift_RecipientList Recipients (To: only)
185   * @param Swift_Address The sender's address
186   * @return int The number sent to
187   */
188  public function send(Swift_Message $message, Swift_RecipientList $recipients, $sender)
189  {
190    $sent = 0;
191    $successive_fails = 0;
192
193    $it = $recipients->getIterator("to");
194    while ($it->hasNext())
195    {
196      $it->next();
197      $recipient = $it->getValue();
198      $tried = 0;
199      $loop = true;
200      while ($loop && $tried < $this->getMaxTries())
201      {
202        try {
203          $tried++;
204          $loop = false;
205          $this->copyMessageHeaders($message);
206          $sent += ($n = $this->swift->send($message, $recipient, $sender));
207          if (!$n) $this->addFailedRecipient($recipient->getAddress());
208          $successive_fails = 0;
209        } catch (Exception $e) {
210          $successive_fails++;
211          $this->restoreMessageHeaders($message);
212          if (($max = $this->getMaxSuccessiveFailures())
213            && $successive_fails > $max)
214          {
215            throw new Exception(
216              "Too many successive failures. BatchMailer is configured to allow no more than " . $max .
217              " successive failures.");
218          }
219          //If an exception was thrown, give it one more go
220          if ($t = $this->getSleepTime()) sleep($t);
221          $this->forceRestartSwift();
222          $loop = true;
223        }
224      }
225    }
226
227    return $sent;
228  }
229}
230