PageRenderTime 29ms CodeModel.GetById 25ms RepoModel.GetById 1ms app.codeStats 0ms

/frontend/php/include/sendmail.php

#
PHP | 510 lines | 291 code | 71 blank | 148 comment | 61 complexity | a5fa8d12da4ece272ec90613638eff82 MD5 | raw file
Possible License(s): AGPL-3.0
  1. <?php
  2. # Every mails sent should be using functions listed here.
  3. #
  4. # <one line to give a brief idea of what this does.>
  5. #
  6. # Copyright 2003-2006 (c) Mathieu Roy <yeupou--gnu.org>
  7. #
  8. # This file is part of Savane.
  9. #
  10. # Savane is free software: you can redistribute it and/or modify
  11. # it under the terms of the GNU Affero General Public License as
  12. # published by the Free Software Foundation, either version 3 of the
  13. # License, or (at your option) any later version.
  14. #
  15. # Savane is distributed in the hope that it will be useful,
  16. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. # GNU Affero General Public License for more details.
  19. #
  20. # You should have received a copy of the GNU Affero General Public License
  21. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  22. # The function that finally send the mail.
  23. # Every mail sent by Savannah should be using that function which
  24. # works like mail().
  25. # Note: $to can be a coma-separated list.
  26. # $from and $to can contain user names
  27. function sendmail_mail ($from,
  28. $to,
  29. $subject,
  30. $message, #4
  31. $savane_project=0,
  32. $savane_tracker=0,
  33. $savane_item_id=0,
  34. $reply_to=0, # 8
  35. $additional_headers=0,
  36. $exclude_list=0)
  37. {
  38. global $int_delayspamcheck;
  39. # Check if $delayspamcheck makes sense
  40. if (!$savane_project || !$savane_tracker || !$savane_item_id)
  41. { unset($int_delayspamcheck); }
  42. # Clean the markup
  43. $message = markup_textoutput($message);
  44. # Make sure the message respect the 78chars max width
  45. # Make also sure we havent got excessive slashes escaping
  46. $message = wordwrap($message, 78);
  47. # Convert ; into
  48. $to = ereg_replace(";", ",", $to);
  49. # Transform $to in an ordered list, without duplicates
  50. # (remove blankspaces)
  51. # FIXME: this is questionable, as it disallow stuff like
  52. # "Dupont Lajoie" <dupont@devnull.net>
  53. # and we should allow that.
  54. # (check on $to necessary because explode returns a one element array in case
  55. # iet has to explode an empty string and this screws the code later on)
  56. if ($to != "")
  57. { $to = array_unique(explode(",", ereg_replace(" ", "", $to))); }
  58. else
  59. { $to = array(); }
  60. # If $from is a login name, write nice From: field
  61. $fromuid = user_getid($from);
  62. if (user_exists($fromuid))
  63. {
  64. $from = user_getrealname($fromuid, 1)." <".user_getemail($fromuid).">";
  65. }
  66. # Write the add. headers
  67. # Note: RFC-821 recommends to use \r\n as line break in headers but \n
  68. # works and there are report of failures with \r\n so we let \n for now.
  69. $more_headers = "From: ".sendmail_encode_header_content($from)."\n";
  70. if ($reply_to)
  71. { $more_headers .= "Reply-To: ".$reply_to."\n"; }
  72. # Add a signature for the server (not if delayed, because it will be added
  73. # we the mail will be actually sent)
  74. if (empty($int_delayspamcheck))
  75. {
  76. $more_headers .= "X-Savane-Server: ".$_SERVER['SERVER_NAME'].":".$_SERVER['SERVER_PORT']." [".$_SERVER['SERVER_ADDR']."]\n";
  77. }
  78. # Necessary for proper utf-8 support
  79. $more_headers .= "MIME-Version: 1.0\n";
  80. $more_headers .= "Content-Type: text/plain;charset=UTF-8\n";
  81. # Savane details
  82. if ($savane_project)
  83. { $more_headers .= "X-Savane-Project: ".$savane_project."\n"; }
  84. if ($savane_tracker)
  85. { $more_headers .= "X-Savane-Tracker: ".$savane_tracker."\n"; }
  86. $savane_comment_id = 0;
  87. if ($savane_item_id)
  88. {
  89. # Look if there is a (internal) comment id set
  90. if (strpos($savane_item_id, ":"))
  91. {
  92. list($savane_item_id, $savane_comment_id) = split(":", $savane_item_id);
  93. }
  94. $more_headers .= "X-Savane-Item-ID: ".$savane_item_id."\n";
  95. }
  96. if ($additional_headers)
  97. { $more_headers .= $additional_headers."\n"; }
  98. # User details.
  99. # Tell what is the user agent, tell which authenticated user made
  100. # the mail to be sent
  101. if (empty($int_delayspamcheck))
  102. {
  103. $more_headers .= "User-Agent: ".$_SERVER['HTTP_USER_AGENT']."\n";
  104. }
  105. if (user_isloggedin())
  106. {
  107. $more_headers .= "X-Apparently-From: ".$_SERVER['REMOTE_ADDR']." (Savane authenticated user ".user_getname(user_getid()).")\n";
  108. }
  109. else
  110. {
  111. $more_headers .= "X-Apparently-From: ".$_SERVER['REMOTE_ADDR']."\n";
  112. }
  113. # Message ID
  114. $msg_id = sendmail_create_msgid();
  115. $more_headers .= "Message-Id: <".$msg_id.">\n";
  116. # Add refs
  117. if ($savane_tracker && $savane_item_id)
  118. {
  119. $more_headers .= "References: ".trackers_get_msgid($savane_tracker, $savane_item_id)."\n";
  120. $more_headers .= "In-Reply-To: ".trackers_get_msgid($savane_tracker, $savane_item_id, true)."\n";
  121. }
  122. # Add a signature for the server (not if delayed, because it will be added
  123. # we the mail will be actually sent)
  124. if (empty($int_delayspamcheck))
  125. {
  126. $message .= "\n\n_______________________________________________
  127. ".sprintf(_("Message sent via/by %s"), $GLOBALS['sys_name'])."
  128. http://".$GLOBALS['sys_default_domain'].$GLOBALS['sys_home']."\n";
  129. }
  130. # Register the message id for future references
  131. if ($savane_tracker && $savane_item_id)
  132. {
  133. trackers_register_msgid($msg_id, $savane_tracker, $savane_item_id);
  134. }
  135. # If there is an exclude list, create an array
  136. # Convert ; into
  137. $exclude = array();
  138. if ($exclude_list)
  139. {
  140. $exclude_list = ereg_replace(";", ",", $exclude_list);
  141. $exclude = array_unique(explode(",", ereg_replace(" ", "", $exclude_list)));
  142. }
  143. while (list(,$v) = each($exclude))
  144. {
  145. if ($v)
  146. { $exclude[$v] = 1; }
  147. }
  148. # Forge the real to list, by parsing every item of the $to list
  149. $recipients = array();
  150. # Do a first run to convert squads by users
  151. $to2 = array();
  152. $squad_seen_before = array();
  153. while (list(,$v) = each($to))
  154. {
  155. if (ctype_digit($v))
  156. { $touid = $v; }
  157. else
  158. { $touid = user_getid($v); }
  159. # Squad exists in the exclude array? Skip it
  160. if (!empty($exclude[$v]))
  161. { continue; }
  162. # Already handled?
  163. if (!empty($squad_seen_before[$v]))
  164. { continue; }
  165. # Record that we handled this already
  166. $squad_seen_before[$v] = true;
  167. # If an address is a squad username, push in all the relevant users
  168. # uid
  169. if (!strpos($v, "@"))
  170. {
  171. if (user_exists($touid, true))
  172. {
  173. # Exists in the exclude array? Skip it
  174. if (is_array($exclude) && array_key_exists(user_getname($touid), $exclude))
  175. { continue; }
  176. # If we get here, we have a squad and we will store all the
  177. # squad members uid
  178. $result_squad = db_execute("SELECT user_id FROM user_squad WHERE squad_id=?",
  179. array($touid));
  180. if ($result_squad && db_numrows($result_squad) > 0)
  181. {
  182. while ($thisuser = db_fetch_array($result_squad))
  183. {
  184. $to2[] = $thisuser['user_id'];
  185. }
  186. }
  187. # No need to go further, this squad was handled
  188. continue;
  189. }
  190. }
  191. # If we get here, it means that we have an address that is not squad
  192. # related have we keep it for the next run
  193. $to2[] = $v;
  194. }
  195. # Second run, we should have only real users here, no squads
  196. $list = array();
  197. $user_subject = array();
  198. $user_name = array();
  199. $seen_before = array();
  200. $i = 0;
  201. while (list(,$v) = each($to2))
  202. {
  203. if (is_numeric($v))
  204. { $touid = $v; }
  205. else
  206. { $touid = user_getid($v); }
  207. # User exists in the exclude array? Skip it
  208. if (!empty($exclude[$v]))
  209. { continue; }
  210. # Already handled?
  211. if (!empty($seen_before[$v]))
  212. { continue; }
  213. # Record that we handled this already
  214. $seen_before[$v] = true;
  215. $i++;
  216. # If an address is a username, get the email address from
  217. # the database.
  218. # If nothing is found, just let the username - there is maybe a
  219. # local alias.
  220. if (!strpos($v, "@"))
  221. {
  222. if (user_exists($touid))
  223. {
  224. # Exists in the exclude array? Skip it
  225. if (is_array($exclude) && array_key_exists(user_getname($touid), $exclude))
  226. { continue; }
  227. $thisuser_email = user_getemail($touid);
  228. # Does the user have a specific subject line?
  229. # FIXME: in the rare case where the user got a specific subject
  230. # line and was added in CC manually, he may receive twice
  231. # the notification, if he is added in realto because his
  232. # email was plenty entered before the entry referring to his
  233. # login is handled.
  234. # If we do check %seen_before just before this, we would
  235. # avoid duplicates but we may loose the notification with
  236. # the user defined subject, which would be worse.
  237. # The only way to handle this would be to cross-check the
  238. # $realto (for instance by using only $seen_before and building
  239. # $realto at the last step) but that would probably be
  240. # overkill.
  241. if (user_get_preference("subject_line", $touid) != "")
  242. {
  243. $list[$i] = $v;
  244. $user_subject[$v] = sendmail_format_subject_line(user_get_preference("subject_line", $touid), $savane_project, $savane_tracker, $savane_item_id)." ".$subject;
  245. $user_name[$v] = user_getrealname($touid, 1)." <".$thisuser_email.">";
  246. $seen_before[$thisuser_email] = true;
  247. continue;
  248. }
  249. # Already handled?
  250. if (!empty($seen_before[$thisuser_email]))
  251. { continue; }
  252. # Record that we handled this already
  253. $seen_before[$thisuser_email] = true;
  254. # Finally, format nicely the entry
  255. $v = user_getrealname($touid, 1)." <".$thisuser_email.">";
  256. }
  257. else
  258. {
  259. # We have a string without @ that is not a user login?
  260. # We assume it could be valid in the mail domain (like a mailing
  261. # list)
  262. # Usually, this is useless, as functions calling
  263. # sendmail_mail() should have already made checks
  264. # (exception: global notifications of trackers)
  265. $seen_before[$v] = true;
  266. $v = utils_normalize_email($v);
  267. # Already handled?
  268. if ($seen_before[$v])
  269. { continue; }
  270. }
  271. }
  272. # FIXME: if at some point we will accept entries like
  273. # "Dupont Lajoie" <dupont@devnull.net>
  274. # we will have to extract "dupont@devnull.net" part and put it
  275. # in %seen_before
  276. # Add addresses arrived so far to the list,
  277. $recipients[] = $v;
  278. # Always record the full string. We may have already saved such info
  279. # before, but we maybe saved strictly the email address, while the
  280. # full string may show up once more. If the full string reappears, we
  281. # wont have to parse it to find the correct email.
  282. $seen_before[$v] = true;
  283. }
  284. # Add eventually info on the subject
  285. if ($savane_tracker && $savane_item_id)
  286. {
  287. $subject = "[".utils_get_tracker_prefix($savane_tracker)." #".$savane_item_id."] ".$subject;
  288. }
  289. # Beuc - 20050316
  290. # That is what I intended to do:
  291. # All newlines should be \r\n; this is apparently more
  292. # RFC821-compliant.
  293. # $message = preg_replace("/(?<!\r)\n/", "\r\n", $message);
  294. # However the opposite is certainly more Mailman-compliant; a bug
  295. # report has been posted to the Mailman team - wait&see [bug #1980]
  296. $message = str_replace("\r\n", "\n", $message);
  297. # Send the mail in UTF-8.
  298. # Normally, nothing non-ASCII should be contained in To: field, apart the
  299. # real names.
  300. # Send the final mail,
  301. $ret = '';
  302. if (count($recipients) > 0)
  303. {
  304. $real_to = sendmail_encode_recipients($recipients);
  305. # Normally, $real_to should not contain duplicates
  306. if (empty($int_delayspamcheck))
  307. {
  308. $ret .= mail($real_to, sendmail_encode_header_content($subject), $message, $more_headers);
  309. // html_feedback_top() is currently escaping HTML
  310. // already, to prevent XSS. So no need to do it again
  311. // here:
  312. //$r = array_map("htmlspecialchars", $recipients);
  313. fb(sprintf(_("Mail sent to %s"), join(', ', $recipients)));
  314. }
  315. else
  316. {
  317. # Wait to be checked for spams
  318. db_autoexecute('trackers_spamcheck_queue_notification',
  319. array(
  320. 'artifact' => $savane_tracker,
  321. 'item_id' => $savane_item_id,
  322. 'comment_id' => $savane_comment_id,
  323. 'to_header' => $real_to,
  324. 'other_headers' => $more_headers,
  325. 'subject_header' => sendmail_encode_header_content($subject),
  326. 'message' => $message
  327. ),
  328. DB_AUTOQUERY_INSERT);
  329. }
  330. }
  331. # Send mails with specific subject line
  332. while (list(,$v) = each($list))
  333. {
  334. if (empty($int_delayspamcheck))
  335. {
  336. $ret .= mail(sendmail_encode_header_content($user_name[$v]), sendmail_encode_header_content($user_subject[$v]), $message, $more_headers);
  337. fb(sprintf(_("Mail sent to %s"), utils_email($user_name[$v], 1)));
  338. }
  339. else
  340. {
  341. # Wait to be checked for spams
  342. db_autoexecute('trackers_spamcheck_queue_notification',
  343. array(
  344. 'artifact' => $savane_tracker,
  345. 'item_id' => $savane_item_id,
  346. 'comment_id' => $savane_comment_id,
  347. 'to_header' => sendmail_encode_header_content($user_name[$v]),
  348. 'other_headers' => $more_headers,
  349. 'subject_header' => sendmail_encode_header_content($user_subject[$v]),
  350. 'message' => $message
  351. ),
  352. DB_AUTOQUERY_INSERT);
  353. }
  354. }
  355. return $ret;
  356. }
  357. # Encode each recipient separately and separate them using commas
  358. function sendmail_encode_recipients ($recipients)
  359. {
  360. $r = array_map("sendmail_encode_header_content", $recipients);
  361. return join(', ', $r);
  362. }
  363. # Needed to send utf-8 headers:
  364. # Take a look at http://www.faqs.org/rfcs/rfc2047.html
  365. # We should use mb_encode_mimeheader() but it just does not work.
  366. #
  367. # We must not encode starting and ending quotes.
  368. # We assume there could be only 2 quotes. Otherwise it would be a malformed
  369. # address.
  370. # The easy way we use to do this is to simply consider as one string the
  371. # content of the quote, if any. If so, we are not working word per word but
  372. # it saves us the time of searching for quotes in every words.
  373. function sendmail_encode_header_content ($header, $charset="UTF-8")
  374. {
  375. $withquotes = FALSE;
  376. if (ereg('"', $header))
  377. {
  378. # quotes found, we each quoted part will be a string to encode
  379. $words = split('"', $header);
  380. $withquotes = 1;
  381. }
  382. else
  383. {
  384. # otherwise, the default behavior is to consider words as strings to
  385. # encode
  386. $words = split(' ', $header);
  387. }
  388. while (list($key,$word) = each($words))
  389. {
  390. # Check word per word if they need encoding
  391. if (!utils_is_ascii($word)) {
  392. $words[$key] = "=?$charset?B?".base64_encode($word)."?=";
  393. }
  394. }
  395. if ($withquotes)
  396. {
  397. return join('"', $words);
  398. }
  399. else
  400. {
  401. return join(' ', $words);
  402. }
  403. }
  404. # A form for logged in users to send mails to others users
  405. function sendmail_form_message ($form_action, $user_id)
  406. {
  407. global $HTML;
  408. print $HTML->box_top(sprintf(_("Send a Message to %s"),user_getrealname($user_id)));
  409. print '<p class="warn">'.("If you are writing for help, did you read the project
  410. documentation first? Try to provide any potentially useful information you can think of.").'</p>';
  411. # We do not really bother finding out the realname + email, sendmail_mail()
  412. # will do it.
  413. print '
  414. <form action="'.$form_action.'" method="post">
  415. <input type="hidden" name="touser" value="'.$user_id.'" />
  416. <input type="hidden" name="fromuser" value="'.user_getname().'" />
  417. <span class="preinput">'._("From:").'</span><br />&nbsp;&nbsp;&nbsp;'.user_getrealname(user_getid(), 1).' &lt;'.user_getemail(user_getid()).'&gt;<br />
  418. <span class="preinput">'._("Mailer:").'</span><br />&nbsp;&nbsp;&nbsp;'.utils_cutstring($_SERVER['HTTP_USER_AGENT'], "50").'<br />
  419. <span class="preinput">'._("Subject:").'</span><br />&nbsp;&nbsp;&nbsp;<input type="text" name="subject" size="60" maxlength="45" value="" /><br />
  420. <span class="preinput">'._("Message:").'</span><br />
  421. &nbsp;&nbsp;&nbsp;<textarea name="body" rows="20" cols="60"></textarea>
  422. <p align="center"><input type="submit" name="send_mail" value="Send Message" /></p>
  423. </form>';
  424. print $HTML->box_bottom();
  425. }
  426. function sendmail_format_subject_line ($subject_line, $savane_project="", $savane_tracker="", $savane_item_id="")
  427. {
  428. $subject_line = ereg_replace("%SERVER", $GLOBALS['sys_default_domain'], $subject_line);
  429. $subject_line = ereg_replace("%PROJECT", $savane_project, $subject_line);
  430. $subject_line = ereg_replace("%TRACKER", $savane_tracker, $subject_line);
  431. return ereg_replace("%ITEM", "#".$savane_item_id, $subject_line);
  432. }
  433. function sendmail_create_msgid ()
  434. {
  435. mt_srand((double)microtime()*1000000);
  436. return date("Ymd-His", time()).".sv".user_getid().".".mt_rand(0,100000)."@".$_SERVER["HTTP_HOST"];
  437. }