mailgun.module 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. <?php
  2. /**
  3. * @file
  4. * Provides integration with Mailgun's email sending API.
  5. */
  6. use Mailgun\Mailgun;
  7. define('MAILGUN_DOCUMENTATION_LINK', 'https://www.drupal.org/node/2547591');
  8. define('MAILGUN_DASHBOARD_LINK', 'https://mailgun.com/app/dashboard');
  9. define('MAILGUN_ADMIN_PAGE', 'admin/config/services/mailgun');
  10. /**
  11. * Implements hook_menu().
  12. */
  13. function mailgun_menu() {
  14. $items = array();
  15. $items[MAILGUN_ADMIN_PAGE] = array(
  16. 'title' => 'Mailgun',
  17. 'description' => 'Configure Mailgun settings.',
  18. 'page callback' => 'drupal_get_form',
  19. 'page arguments' => array('mailgun_admin_settings'),
  20. 'access arguments' => array('administer mailgun'),
  21. 'file' => 'mailgun.admin.inc',
  22. );
  23. $items[MAILGUN_ADMIN_PAGE . '/settings'] = array(
  24. 'title' => 'Settings',
  25. 'type' => MENU_DEFAULT_LOCAL_TASK,
  26. 'weight' => 0,
  27. );
  28. $items[MAILGUN_ADMIN_PAGE . '/test'] = array(
  29. 'title' => 'Send test email',
  30. 'page callback' => 'drupal_get_form',
  31. 'page arguments' => array('mailgun_test_form'),
  32. 'access arguments' => array('administer mailgun'),
  33. 'description' => 'Send a test e-mail using the Mailgun API.',
  34. 'file' => 'mailgun.admin.inc',
  35. 'type' => MENU_LOCAL_TASK,
  36. 'weight' => 1,
  37. );
  38. return $items;
  39. }
  40. /**
  41. * Implements hook_permission().
  42. */
  43. function mailgun_permission() {
  44. return array(
  45. 'administer mailgun' => array(
  46. 'title' => t('Administer Mailgun'),
  47. 'description' => t('Perform administration tasks for the Mailgun e-mail sending service.'),
  48. 'restrict access' => TRUE,
  49. ),
  50. );
  51. }
  52. /**
  53. * Implements hook_theme().
  54. */
  55. function mailgun_theme($existing, $type, $theme, $path) {
  56. return array(
  57. 'mailgun_message' => array(
  58. 'variables' => array(
  59. 'subject' => NULL,
  60. 'body' => NULL,
  61. 'message' => array(),
  62. ),
  63. 'template' => 'mailgun-message',
  64. 'path' => drupal_get_path('module', 'mailgun') . '/templates',
  65. 'mail theme' => TRUE,
  66. ),
  67. );
  68. }
  69. /**
  70. * Implements hook_help().
  71. */
  72. function mailgun_help($path, $arg) {
  73. switch ($path) {
  74. case MAILGUN_ADMIN_PAGE:
  75. return '<p>' . t('See !link for instructions on installing and configuring Mailgun.', array(
  76. '!link' => l(t('documentation'), MAILGUN_DOCUMENTATION_LINK),
  77. )) . '</p>';
  78. case MAILGUN_ADMIN_PAGE . '/test':
  79. return '<p>' . t('Use this form to send a test e-mail to ensure you have correctly configured Mailgun.') . '</p>';
  80. }
  81. }
  82. /**
  83. * Implements hook_cron_queue_info().
  84. */
  85. function mailgun_cron_queue_info() {
  86. $queues = array();
  87. $queues['mailgun_queue'] = array(
  88. 'worker callback' => 'mailgun_send',
  89. 'time' => 60,
  90. );
  91. return $queues;
  92. }
  93. /**
  94. * Implements hook_mail().
  95. */
  96. function mailgun_mail($key, &$message, $params) {
  97. switch ($key) {
  98. case 'test':
  99. $message['subject'] = t('Mailgun test email');
  100. $message['body'] = $params['message'];
  101. if ($params['attachment']) {
  102. $message['params']['attachments'] = array(drupal_realpath('misc/druplicon.png'));
  103. }
  104. break;
  105. }
  106. }
  107. /**
  108. * Implements hook_libraries_info().
  109. */
  110. function mailgun_libraries_info() {
  111. $libraries['mailgun'] = array(
  112. 'name' => 'Mailgun PHP library',
  113. 'vendor url' => 'https://documentation.mailgun.com/en/latest/libraries.html#php',
  114. 'download url' => 'https://github.com/mailgun/mailgun-php',
  115. 'version arguments' => array(
  116. 'file' => 'vendor/mailgun/mailgun-php/CHANGELOG.md',
  117. 'pattern' => '/##\W+((\d+)\.(\d+))/',
  118. ),
  119. // Path to the 'autoload.php' created by Composer.
  120. 'path' => 'vendor',
  121. 'files' => array(
  122. 'php' => array('autoload.php'),
  123. ),
  124. );
  125. return $libraries;
  126. }
  127. /**
  128. * Implements hook_form_FORM_ID_alter().
  129. */
  130. function mailgun_form_libraries_admin_library_status_form_alter(&$form, &$form_state, $form_id) {
  131. $library = drupal_array_get_nested_value($form_state, array(
  132. 'build_info', 'args', 0,
  133. ));
  134. if (empty($library['machine name']) || $library['machine name'] !== 'mailgun') {
  135. return;
  136. }
  137. // Libraries module provides own instruction "How to install the library".
  138. // We override it because this instruction is not correct and may confuse.
  139. $form['instructions'] = array(
  140. '#markup' => t('The Mailgun PHP library is not installed. Please see Installation section in the !link.', array(
  141. '!link' => l(t('documentation'), MAILGUN_DOCUMENTATION_LINK),
  142. )),
  143. );
  144. }
  145. /**
  146. * Get the Mailgun client to access Mailgun's endpoints.
  147. *
  148. * @param string $key
  149. * The Mailgun API key. Leave empty to use the API key saved in database.
  150. *
  151. * @return \Mailgun\Mailgun
  152. * Mailgun object.
  153. */
  154. function mailgun_get_client($key = '') {
  155. // Check if the Mailgun PHP library is installed.
  156. if (!mailgun_check_library()) {
  157. watchdog('mailgun', 'Mailgun client initialization failed: Unable to load the Mailgun PHP library.', array(), WATCHDOG_ERROR);
  158. return FALSE;
  159. }
  160. $key = (empty($key)) ? variable_get('mailgun_api_key', '') : $key;
  161. if (empty($key)) {
  162. watchdog('mailgun', 'Mailgun client initialization failed: Missing API key.', array(), WATCHDOG_ERROR);
  163. return FALSE;
  164. }
  165. return Mailgun::create($key);
  166. }
  167. /**
  168. * Detect if Mailgun library is installed.
  169. *
  170. * @return bool
  171. * TRUE if library is installed, FALSE otherwise.
  172. */
  173. function mailgun_check_library() {
  174. if (module_exists('libraries')) {
  175. libraries_load('mailgun');
  176. }
  177. if (method_exists('\Mailgun\Mailgun', 'create')) {
  178. return TRUE;
  179. }
  180. return FALSE;
  181. }
  182. /**
  183. * Prepares variables for mailgun-message.tpl.php.
  184. *
  185. * Adds id/module/key-specific hook suggestions.
  186. *
  187. * @see templates/mailgun-message.tpl.php
  188. */
  189. function template_preprocess_mailgun_message(&$variables) {
  190. $variables['theme_hook_suggestions'][] = 'mailgun_message__' . $variables['message']['id'];
  191. $variables['theme_hook_suggestions'][] = 'mailgun_message__' . $variables['message']['module'];
  192. $variables['theme_hook_suggestions'][] = 'mailgun_message__' . $variables['message']['key'];
  193. }
  194. /**
  195. * Send an e-mail using the Mailgun API.
  196. *
  197. * @param array $mailgun_message
  198. * A Mailgun message array. Contains the following keys:
  199. * - from: The e-mail addressthe message will be sent from.
  200. * - to: The e-mail addressthe message will be sent to.
  201. * - subject: The subject of the message.
  202. * - text: The plain-text version of the message. Processed using
  203. * drupal_html_to_text().
  204. * - html: The original message content. May contain HTML tags.
  205. * - cc: One or more carbon copy recipients. If multiple, separate with
  206. * commas.
  207. * - bcc: One or more blind carbon copy recipients. If multiple, separate
  208. * with commas.
  209. * - o:tag: An array containing the tags to add to the message.
  210. * See: https://documentation.mailgun.com/user_manual.html#tagging.
  211. * - o:campaign: The campaign ID this message belongs to.
  212. * https://documentation.mailgun.com/user_manual.html#um-campaign-analytics
  213. * - o:deliverytime: Desired time of delivery. Messages can be scheduled for
  214. * a maximum of 3 days in the future.
  215. * See: https://documentation.mailgun.com/api-intro.html#date-format.
  216. * - o:dkim: Boolean indicating whether or not to enable DKIM signatures on
  217. * per-message basis.
  218. * - o:testmode: Boolean indicating whether or not to enable test mode.
  219. * See: https://documentation.mailgun.com/user_manual.html#manual-testmode.
  220. * - o:tracking: Boolean indicating whether or not to toggle tracking on a
  221. * per-message basis.
  222. * See: https://documentation.mailgun.com/user_manual.html#tracking-messages.
  223. * - o:tracking-clicks: Boolean or string "htmlonly" indicating whether or
  224. * not to toggle clicks tracking on a per-message basis. Has higher
  225. * priority than domain-level setting.
  226. * - o:tracking-opens: Boolean indicating whether or not to toggle clicks
  227. * tracking on a per-message basis. Has higher priority than domain-level
  228. * setting.
  229. * - h:X-My-Header: h: prefix followed by an arbitrary value allows to append
  230. * a custom MIME header to the message (X-My-Header in this case).
  231. * For example, h:Reply-To to specify Reply-To address.
  232. * - v:my-var: v: prefix followed by an arbitrary name allows to attach a
  233. * custom JSON data to the message.
  234. * See: https://documentation.mailgun.com/user_manual.html#manual-customdata.
  235. *
  236. * @return bool
  237. * TRUE if the mail was successfully accepted, FALSE otherwise.
  238. */
  239. function mailgun_send(array $mailgun_message) {
  240. $client = mailgun_get_client();
  241. if (!$client) {
  242. return FALSE;
  243. }
  244. // Test mode. Mailgun will accept the message but will not send it.
  245. if (variable_get('mailgun_test', FALSE)) {
  246. $mailgun_message['o:testmode'] = 'yes';
  247. }
  248. // Merge the $mailgun_message array with options.
  249. $mailgun_message += $mailgun_message['params'];
  250. unset($mailgun_message['params']);
  251. if (variable_get('mailgun_domain', '_sender') === '_sender') {
  252. // Extract the domain from the sender's email address.
  253. // Use regular expression to check since it could be either a plain email
  254. // address or in the form "Name <example@example.com>".
  255. $tokens = (preg_match('/^\s*(.+?)\s*<\s*([^>]+)\s*>$/', $mailgun_message['from'], $matches) === 1) ? explode('@', $matches[2]) : explode('@', $mailgun_message['from']);
  256. $mail_domain = array_pop($tokens);
  257. // Retrieve a list of available domains first.
  258. $domains = array();
  259. try {
  260. $result = $client->domains()->index();
  261. if (!empty($result)) {
  262. foreach ($result->getDomains() as $domain) {
  263. $domains[$domain->getName()] = $domain->getName();
  264. }
  265. }
  266. else {
  267. watchdog('mailgun', 'Could not retrieve domain list.', array(), WATCHDOG_ERROR);
  268. }
  269. }
  270. catch (Exception $e) {
  271. watchdog('mailgun', 'An exception occurred while retrieving domains. @code: @message', array(
  272. '@code' => $e->getCode(),
  273. '@message' => $e->getMessage(),
  274. ), WATCHDOG_ERROR);
  275. }
  276. if (empty($domains)) {
  277. // No domain available.
  278. // Although this shouldn't happen, doesn't hurt to check.
  279. return FALSE;
  280. }
  281. // Now, we need to get the working domain. This is generally the domain the
  282. // From address is on or the root domain of it.
  283. $working_domain = '';
  284. if (in_array($mail_domain, $domains, TRUE)) {
  285. // Great. Found it.
  286. $working_domain = $mail_domain;
  287. }
  288. else {
  289. // Oops. No match. Perhaps it's a subdomain instead.
  290. foreach ($domains as $domain) {
  291. if (strpos($domain, $mail_domain) !== FALSE) {
  292. // Got it.
  293. $working_domain = $domain;
  294. break;
  295. }
  296. }
  297. }
  298. // There is a chance that the user is attempting to send from an email
  299. // address that's on a domain not yet added to the Mailgun account.
  300. // In that case, abort sending and report error.
  301. if (empty($working_domain)) {
  302. watchdog('mailgun', 'Unable to locate a working domain for From address %mail. Aborting sending.', array(
  303. '%mail' => $mailgun_message['from'],
  304. ), WATCHDOG_ERROR);
  305. return FALSE;
  306. }
  307. }
  308. else {
  309. $working_domain = variable_get('mailgun_domain', '');
  310. }
  311. // Send message with attachments.
  312. if (!empty($mailgun_message['attachment'])) {
  313. foreach ($mailgun_message['attachment'] as &$attachment) {
  314. // Ignore array constructions. Not sure what values can be here.
  315. if (is_array($attachment)) {
  316. continue;
  317. }
  318. $attachment = array('filePath' => $attachment);
  319. }
  320. }
  321. try {
  322. $result = $client->messages()->send($working_domain, $mailgun_message);
  323. if (!empty($result)) {
  324. if (variable_get('mailgun_log', FALSE)) {
  325. watchdog('mailgun', 'Successfully sent message from %from to %to. %message.', array(
  326. '%from' => $mailgun_message['from'],
  327. '%to' => $mailgun_message['to'],
  328. '%message' => $result->getMessage(),
  329. ));
  330. }
  331. return TRUE;
  332. }
  333. else {
  334. watchdog('mailgun', 'Failed to send message from %from to %to. %message.', array(
  335. '%from' => $mailgun_message['from'],
  336. '%to' => $mailgun_message['to'],
  337. '%message' => $result->getMessage(),
  338. ), WATCHDOG_ERROR);
  339. return FALSE;
  340. }
  341. }
  342. catch (Exception $e) {
  343. watchdog('mailgun', 'Exception occurred while trying to send test email from %from to %to. @code: @message.', array(
  344. '%from' => $mailgun_message['from'],
  345. '%to' => $mailgun_message['to'],
  346. '@code' => $e->getCode(),
  347. '@message' => $e->getMessage(),
  348. ));
  349. return FALSE;
  350. }
  351. }