PageRenderTime 49ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Postmark/Mail.php

http://github.com/Znarkus/postmark-php
PHP | 638 lines | 352 code | 85 blank | 201 comment | 46 complexity | 4ce3a3f888c86dc770d985e383a77377 MD5 | raw file
  1. <?php
  2. namespace Postmark;
  3. use ReflectionClass, Mail_Postmark_Adapter, InvalidArgumentException, OverflowException, Exception, BadMethodCallException;
  4. /**
  5. * Postmark PHP class
  6. *
  7. * Copyright 2011, Markus Hedlund, Mimmin AB, www.mimmin.com
  8. * Licensed under the MIT License.
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @author Markus Hedlund (markus@mimmin.com) at mimmin (www.mimmin.com)
  12. * @copyright Copyright 2009 - 2011, Markus Hedlund, Mimmin AB, www.mimmin.com
  13. * @version 0.5
  14. * @license http://www.opensource.org/licenses/mit-license.php The MIT License
  15. */
  16. class Mail
  17. {
  18. const DEBUG_OFF = 0;
  19. const DEBUG_VERBOSE = 1;
  20. const DEBUG_RETURN = 2;
  21. const TESTING_API_KEY = 'POSTMARK_API_TEST';
  22. const MAX_ATTACHMENT_SIZE = 10485760; // 10 MB
  23. const RECIPIENT_TYPE_TO = 'to';
  24. const RECIPIENT_TYPE_CC = 'cc';
  25. const RECIPIENT_TYPE_BCC = 'bcc';
  26. static $_mimeTypes = array('ai' => 'application/postscript', 'avi' => 'video/x-msvideo', 'doc' => 'application/msword', 'eps' => 'application/postscript', 'gif' => 'image/gif', 'htm' => 'text/html', 'html' => 'text/html', 'jpeg' => 'image/jpeg', 'jpg' => 'image/jpeg', 'mov' => 'video/quicktime', 'mp3' => 'audio/mpeg', 'mpg' => 'video/mpeg', 'pdf' => 'application/pdf', 'ppt' => 'application/vnd.ms-powerpoint', 'ps' => 'application/postscript', 'rtf' => 'application/rtf', 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'txt' => 'text/plain', 'xls' => 'application/vnd.ms-excel', 'csv' => 'text/comma-separated-values', 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'flv' => 'video/x-flv', 'ics' => 'text/calendar', 'log' => 'text/plain', 'png' => 'image/png', 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'psd' => 'image/photoshop', 'rm' => 'application/vnd.rn-realmedia', 'swf' => 'application/x-shockwave-flash', 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'xml' => 'text/xml');
  27. private $_apiKey;
  28. private $_from;
  29. private $_to = array();
  30. private $_cc = array();
  31. private $_bcc = array();
  32. private $_replyTo;
  33. private $_subject;
  34. private $_tag;
  35. private $_messagePlain;
  36. private $_messageHtml;
  37. private $_trackOpens;
  38. private $_headers = array();
  39. private $_attachments = array();
  40. private $_debugMode = self::DEBUG_OFF;
  41. /**
  42. * Initialize
  43. *
  44. * @param string $apiKey Postmark server API key
  45. * @return void
  46. */
  47. public function __construct($apiKey)
  48. {
  49. if (class_exists('Mail_Postmark_Adapter', false)) {
  50. $reflection = new ReflectionClass('Mail_Postmark_Adapter');
  51. if (!$reflection->implementsInterface('MailAdapterInterface')) {
  52. trigger_error('Mail_Postmark_Adapter must implement interface MailAdapterInterface', E_USER_ERROR);
  53. }
  54. $this->_apiKey = Mail_Postmark_Adapter::getApiKey();
  55. Mail_Postmark_Adapter::setupDefaults($this);
  56. } else {
  57. $this->_apiKey = $apiKey;
  58. }
  59. $this->messageHtml(null)->messagePlain(null);
  60. }
  61. /**
  62. * Add a physical file as an attachment
  63. * Options:
  64. * - filenameAlias, use a different filename for the attachment
  65. *
  66. * @param string $filename Location of the file
  67. * @param array $options An optional array with options
  68. * @throws InvalidArgumentException If file doesn't exist
  69. * @throws OverflowException If maximum attachment size has been reached
  70. * @return Mail
  71. */
  72. public function &addAttachment($filename, $options = array())
  73. {
  74. if (!is_file($filename)) {
  75. throw new InvalidArgumentException("File \"{$filename}\" does not exist");
  76. }
  77. $this->addCustomAttachment(
  78. isset($options['filenameAlias']) ? $options['filenameAlias'] : basename($filename),
  79. file_get_contents($filename),
  80. $this->_getMimeType($filename)
  81. );
  82. return $this;
  83. }
  84. /**
  85. * Add a BCC address
  86. *
  87. * @param string $address E-mail address used in BCC
  88. * @param string $name Optional. Name used in BCC
  89. * @throws InvalidArgumentException On invalid address
  90. * @throws OverflowException If there are too many email recipients
  91. * @return Mail
  92. */
  93. public function &addBcc($address, $name = null)
  94. {
  95. $this->_addRecipient(self::RECIPIENT_TYPE_BCC, $address, $name);
  96. return $this;
  97. }
  98. /**
  99. * Add a CC address
  100. *
  101. * @param string $address E-mail address used in CC
  102. * @param string $name Optional. Name used in CC
  103. * @throws InvalidArgumentException On invalid address
  104. * @throws OverflowException If there are too many email recipients
  105. * @return Mail
  106. */
  107. public function &addCc($address, $name = null)
  108. {
  109. $this->_addRecipient(self::RECIPIENT_TYPE_CC, $address, $name);
  110. return $this;
  111. }
  112. /**
  113. * Add an attachment.
  114. *
  115. * @param string $filename What to call the file
  116. * @param string $content Raw file data
  117. * @param string $mimeType The mime type of the file
  118. * @throws OverflowException If maximum attachment size has been reached
  119. * @return Mail
  120. */
  121. public function &addCustomAttachment($filename, $content, $mimeType)
  122. {
  123. $length = strlen($content);
  124. $lengthSum = 0;
  125. foreach ($this->_attachments as $file) {
  126. $lengthSum += $file['length'];
  127. }
  128. if ($lengthSum + $length > self::MAX_ATTACHMENT_SIZE) {
  129. throw new OverflowException("Maximum attachment size reached");
  130. }
  131. $this->_attachments[$filename] = array(
  132. 'content' => base64_encode($content),
  133. 'mimeType' => $mimeType,
  134. 'length' => $length
  135. );
  136. return $this;
  137. }
  138. /**
  139. * Add a custom header
  140. *
  141. * @param string $name Custom header name
  142. * @param string $value Custom header value
  143. * @return Mail
  144. */
  145. public function &addHeader($name, $value)
  146. {
  147. $this->_headers[$name] = $value;
  148. return $this;
  149. }
  150. /**
  151. * Add a receiver
  152. *
  153. * @param string $address E-mail address used in To
  154. * @param string $name Optional. Name used in To
  155. * @throws InvalidArgumentException On invalid address
  156. * @throws OverflowException If there are too many email recipients
  157. * @return Mail
  158. */
  159. public function &addTo($address, $name = null)
  160. {
  161. $this->_addRecipient(self::RECIPIENT_TYPE_TO, $address, $name);
  162. return $this;
  163. }
  164. /**
  165. * New e-mail
  166. *
  167. * @param string $apiKey Postmark server API key
  168. * @return Mail
  169. */
  170. public static function compose($apiKey)
  171. {
  172. return new self($apiKey);
  173. }
  174. /**
  175. * Turns debug output on
  176. *
  177. * @param int $mode One of the debug constants
  178. * @return Mail
  179. */
  180. public function &debug($mode = self::DEBUG_VERBOSE)
  181. {
  182. $this->_debugMode = $mode;
  183. return $this;
  184. }
  185. /**
  186. * Specify sender. Overwrites default From. Note that the address
  187. * must first be added in the Postmark app admin interface
  188. *
  189. * @param string $address E-mail address used in From
  190. * @param string $name Optional. Name used in From
  191. * @throws InvalidArgumentException On invalid address
  192. * @return Mail
  193. */
  194. public function &from($address, $name = null)
  195. {
  196. if (!$this->_validateAddress($address)) {
  197. throw new InvalidArgumentException("From address \"{$address}\" is invalid");
  198. }
  199. $this->_from = array('address' => $address, 'name' => $name);
  200. return $this;
  201. }
  202. /**
  203. * Specify sender name. Overwrites default From name, but doesn't change address.
  204. *
  205. * @param string $name Name used in From
  206. * @return Mail
  207. */
  208. public function &fromName($name)
  209. {
  210. $this->_from['name'] = $name;
  211. return $this;
  212. }
  213. /**
  214. * Add HTML message. Can be used in conjunction with messagePlain()
  215. *
  216. * @param string $message E-mail message
  217. * @return Mail
  218. */
  219. public function &messageHtml($message)
  220. {
  221. $this->_messageHtml = $message;
  222. return $this;
  223. }
  224. /**
  225. * Add plaintext message. Can be used in conjunction with messageHtml()
  226. * @param string $message E-mail message
  227. * @return Mail
  228. */
  229. public function &messagePlain($message)
  230. {
  231. $this->_messagePlain = $message;
  232. return $this;
  233. }
  234. /**
  235. * Specify reply-to
  236. *
  237. * @param string $address E-mail address used in To
  238. * @param string $name Optional. Name used in To
  239. * @throws InvalidArgumentException On invalid address
  240. * @return Mail
  241. */
  242. public function &replyTo($address, $name = null)
  243. {
  244. if (!$this->_validateAddress($address)) {
  245. throw new InvalidArgumentException("Reply To address \"{$address}\" is invalid");
  246. }
  247. $this->_replyTo = array('address' => $address, 'name' => $name);
  248. return $this;
  249. }
  250. /**
  251. * Sends the e-mail. Prints debug output if debug mode is turned on
  252. *
  253. * Options:
  254. * returnMessageId
  255. *
  256. * @throws Exception If HTTP code 422, Exception with API error code and Postmark message, otherwise HTTP code.
  257. * @throws BadMethodCallException If From address, To address or Subject is missing
  258. * @return boolean - True if success and $returnID is false.
  259. * @return string - if $returnID is true and one message is sent.
  260. * @return array - if DEBUG_RETURN is enabled.
  261. */
  262. public function send($options = array())
  263. {
  264. $this->_validateData();
  265. $data = $this->_prepareData();
  266. $headers = array(
  267. 'Accept: application/json',
  268. 'Content-Type: application/json',
  269. 'X-Postmark-Server-Token: ' . $this->_apiKey
  270. );
  271. $ch = curl_init();
  272. curl_setopt($ch, CURLOPT_URL, 'https://api.postmarkapp.com/email');
  273. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  274. curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
  275. curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
  276. curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
  277. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
  278. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
  279. curl_setopt($ch, CURLOPT_CAINFO, dirname(__FILE__) . '/Certificate/cacert.pem');
  280. $return = curl_exec($ch);
  281. $curlError = curl_error($ch);
  282. $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  283. $this->_log(array(
  284. 'messageData' => $data,
  285. 'return' => $return,
  286. 'curlError' => $curlError,
  287. 'httpCode' => $httpCode
  288. ));
  289. if (($this->_debugMode & self::DEBUG_VERBOSE) === self::DEBUG_VERBOSE) {
  290. echo "JSON: " . json_encode($data)
  291. . "\nHeaders: \n\t" . implode("\n\t", $headers)
  292. . "\nReturn:\n{$return}"
  293. . "\nCurl error: {$curlError}"
  294. . "\nHTTP code: {$httpCode}";
  295. }
  296. if ($curlError !== '') {
  297. throw new Exception($curlError);
  298. }
  299. if (!$this->_isTwoHundred($httpCode)) {
  300. if ($httpCode == 422) {
  301. $return = json_decode($return);
  302. throw new Exception($return->Message, $return->ErrorCode);
  303. } else {
  304. throw new Exception("Error while mailing. Postmark returned HTTP code {$httpCode} with message \"{$return}\"", $httpCode);
  305. }
  306. }
  307. if (($this->_debugMode & self::DEBUG_RETURN) === self::DEBUG_RETURN) {
  308. return array(
  309. 'json' => json_encode($data),
  310. 'headers' => $headers,
  311. 'return' => $return,
  312. 'curlError' => $curlError,
  313. 'httpCode' => $httpCode
  314. );
  315. }
  316. // Return the ID of the message sent if the option is set.
  317. if(!empty($options['returnMessageId'])) {
  318. $messageInformation = json_decode($return);
  319. return $messageInformation->MessageID;
  320. }
  321. return true;
  322. }
  323. /**
  324. * Specify subject
  325. *
  326. * @param string $subject E-mail subject
  327. * @return Mail
  328. */
  329. public function &subject($subject)
  330. {
  331. $this->_subject = $subject;
  332. return $this;
  333. }
  334. /**
  335. * You can categorize outgoing email using the optional Tag property.
  336. * If you use different tags for the different types of emails your
  337. * application generates, you will be able to get detailed statistics
  338. * for them through the Postmark user interface.
  339. * Only 1 tag per email is supported.
  340. *
  341. * @param string $tag One tag
  342. * @return Mail
  343. */
  344. public function &tag($tag)
  345. {
  346. $this->_tag = $tag;
  347. return $this;
  348. }
  349. /**
  350. * Turns email tracking on
  351. *
  352. * @return Mail
  353. */
  354. public function &trackOpen()
  355. {
  356. $this->_trackOpens = true;
  357. return $this;
  358. }
  359. /**
  360. * Specify receiver. Use addTo to add more.
  361. *
  362. * @deprecated Use addTo.
  363. * @param string $address E-mail address used in To
  364. * @param string $name Optional. Name used in To
  365. * @return Mail
  366. */
  367. public function &to($address, $name = null)
  368. {
  369. $this->_to = array();
  370. $this->addTo($address, $name);
  371. return $this;
  372. }
  373. /**
  374. * @param string $type Either 'to', 'cc' or 'bcc'
  375. * @param string $address
  376. * @param string|null $name
  377. * @throws InvalidArgumentException On invalid address
  378. * @throws OverflowException If there are too many email recipients
  379. */
  380. public function _addRecipient($type, $address, $name = null)
  381. {
  382. $address = trim($address);
  383. if (!$this->_validateAddress($address)) {
  384. throw new InvalidArgumentException("Address \"{$address}\" is invalid");
  385. }
  386. if (count($this->_to) + count($this->_cc) + count($this->_bcc) === 20) {
  387. throw new OverflowException('Too many email recipients');
  388. }
  389. $data = array('address' => $address, 'name' => $name);
  390. switch ($type) {
  391. case self::RECIPIENT_TYPE_TO:
  392. $this->_to[] = $data;
  393. break;
  394. case self::RECIPIENT_TYPE_CC:
  395. $this->_cc[] = $data;
  396. break;
  397. case self::RECIPIENT_TYPE_BCC:
  398. $this->_bcc[] = $data;
  399. break;
  400. }
  401. }
  402. private function _createAddress($address, $name = null)
  403. {
  404. if (isset($name)) {
  405. return '"' . str_replace('"', '', $name) . '" <' . $address . '>';
  406. } else {
  407. return $address;
  408. }
  409. }
  410. /**
  411. * Try to detect the mime type
  412. * @param $filename
  413. * @return string
  414. */
  415. private function _getMimeType($filename)
  416. {
  417. $extension = pathinfo($filename, PATHINFO_EXTENSION);
  418. if (isset(self::$_mimeTypes[$extension])) {
  419. return self::$_mimeTypes[$extension];
  420. } else if (function_exists('mime_content_type')) {
  421. return mime_content_type($filename);
  422. } else if (function_exists('finfo_file')) {
  423. $fh = finfo_open(FILEINFO_MIME);
  424. $mime = finfo_file($fh, $filename);
  425. finfo_close($fh);
  426. return $mime;
  427. } else if ($image = getimagesize($filename)) {
  428. return $image[2];
  429. } else {
  430. return 'application/octet-stream';
  431. }
  432. }
  433. /**
  434. * If a number is 200-299
  435. * @param $value
  436. * @return bool
  437. */
  438. private function _isTwoHundred($value)
  439. {
  440. return intval($value / 100) == 2;
  441. }
  442. /**
  443. * Call the logger method, if one exists
  444. *
  445. * @param array $logData
  446. */
  447. private function _log($logData)
  448. {
  449. if (class_exists('Mail_Postmark_Adapter', false)) {
  450. Mail_Postmark_Adapter::log($logData);
  451. }
  452. }
  453. /**
  454. * Prepares the data array
  455. * @return array
  456. */
  457. private function _prepareData()
  458. {
  459. $data = array(
  460. 'Subject' => $this->_subject
  461. );
  462. $data['From'] = $this->_createAddress($this->_from['address'], $this->_from['name']);
  463. $data['To'] = array();
  464. $data['Cc'] = array();
  465. $data['Bcc'] = array();
  466. foreach ($this->_to as $to) {
  467. $data['To'][] = $this->_createAddress($to['address'], $to['name']);
  468. }
  469. foreach ($this->_cc as $cc) {
  470. $data['Cc'][] = $this->_createAddress($cc['address'], $cc['name']);
  471. }
  472. foreach ($this->_bcc as $bcc) {
  473. $data['Bcc'][] = $this->_createAddress($bcc['address'], $bcc['name']);
  474. }
  475. $data['To'] = implode(', ', $data['To']);
  476. if (empty($data['Cc'])) {
  477. unset($data['Cc']);
  478. } else {
  479. $data['Cc'] = implode(', ', $data['Cc']);
  480. }
  481. if (empty($data['Bcc'])) {
  482. unset($data['Bcc']);
  483. } else {
  484. $data['Bcc'] = implode(', ', $data['Bcc']);
  485. }
  486. if ($this->_replyTo !== null) {
  487. $data['ReplyTo'] = $this->_createAddress($this->_replyTo['address'], $this->_replyTo['name']);
  488. }
  489. if ($this->_messageHtml !== null) {
  490. $data['HtmlBody'] = $this->_messageHtml;
  491. }
  492. if ($this->_messagePlain !== null) {
  493. $data['TextBody'] = $this->_messagePlain;
  494. }
  495. if ($this->_tag !== null) {
  496. $data['Tag'] = $this->_tag;
  497. }
  498. if (!empty($this->_headers)) {
  499. $data['Headers'] = array();
  500. foreach ($this->_headers as $name => $value) {
  501. $data['Headers'][] = array('Name' => $name, 'Value' => $value);
  502. }
  503. }
  504. if (!empty($this->_attachments)) {
  505. $data['Attachments'] = array();
  506. foreach ($this->_attachments as $filename => $file) {
  507. $data['Attachments'][] = array(
  508. 'Name' => $filename,
  509. 'Content' => $file['content'],
  510. 'ContentType' => $file['mimeType']
  511. );
  512. }
  513. }
  514. if ($this->_trackOpens !== null) {
  515. if ($this->_trackOpens === true) {
  516. $data['TrackOpens'] = 'true';
  517. }
  518. }
  519. return $data;
  520. }
  521. /**
  522. * Validates an e-mail address
  523. * @param $email
  524. * @return bool
  525. */
  526. private function _validateAddress($email)
  527. {
  528. // http://php.net/manual/en/function.filter-var.php
  529. // return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
  530. // filter_var proved to be unworthy (passed foo..bar@domain.com as valid),
  531. // and was therefore replace with
  532. $regex = "/^([\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*[\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+@((((([a-z0-9]{1}[a-z0-9\-]{0,62}[a-z0-9]{1})|[a-z])\.)+[a-z]{2,6})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?)$/i";
  533. // from http://fightingforalostcause.net/misc/2006/compare-email-regex.php
  534. return preg_match($regex, $email) === 1;
  535. }
  536. /**
  537. * Validate that the email can be sent
  538. *
  539. * @throws BadMethodCallException If From address, To address or Subject is missing
  540. */
  541. private function _validateData()
  542. {
  543. if ($this->_from['address'] === null) {
  544. throw new BadMethodCallException('From address is not set');
  545. }
  546. if (empty($this->_to)) {
  547. throw new BadMethodCallException('No To address is set');
  548. }
  549. if (!isset($this->_subject)) {
  550. throw new BadMethodCallException('Subject is not set');
  551. }
  552. }
  553. }