notify.py 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189
  1. # -*- coding: utf-8 -*-
  2. """
  3. (c) 2014-2018 - Copyright Red Hat Inc
  4. Authors:
  5. Pierre-Yves Chibon <pingou@pingoured.fr>
  6. pagure notifications.
  7. """
  8. from __future__ import print_function, unicode_literals, absolute_import
  9. # pylint: disable=too-many-branches
  10. # pylint: disable=too-many-arguments
  11. import datetime
  12. import hashlib
  13. import json
  14. import logging
  15. import os
  16. import re
  17. import smtplib
  18. import time
  19. import six
  20. import ssl
  21. from email.header import Header
  22. from email.mime.text import MIMEText
  23. from six.moves.urllib_parse import urljoin
  24. import blinker
  25. import flask
  26. import pagure.lib.query
  27. import pagure.lib.tasks_services
  28. from pagure.config import config as pagure_config
  29. from pagure.pfmarkdown import MENTION_RE
  30. from markdown.extensions.fenced_code import FencedBlockPreprocessor
  31. _log = logging.getLogger(__name__)
  32. REPLY_MSG = "To reply, visit the link below"
  33. if pagure_config["EVENTSOURCE_SOURCE"]:
  34. REPLY_MSG += " or just reply to this email"
  35. def fedmsg_publish(*args, **kwargs): # pragma: no cover
  36. """ Try to publish a message on the fedmsg bus. """
  37. if not pagure_config.get("FEDMSG_NOTIFICATIONS", True):
  38. return
  39. _log.warning(
  40. "fedmsg support is being deprecated in favor of fedora-messaging "
  41. "you likely want to stop relying on it as it will disapear in the "
  42. "future, most likely in the 6.0 release"
  43. )
  44. # We catch Exception if we want :-p
  45. # pylint: disable=broad-except
  46. # Ignore message about fedmsg import
  47. # pylint: disable=import-error
  48. kwargs["modname"] = "pagure"
  49. kwargs["cert_prefix"] = "pagure"
  50. kwargs["active"] = True
  51. try:
  52. import fedmsg
  53. fedmsg.publish(*args, **kwargs)
  54. except Exception:
  55. _log.exception("Error sending fedmsg")
  56. def fedora_messaging_publish(topic, message): # pragma: no cover
  57. """ Try to publish a message on AMQP using fedora-messaging. """
  58. if not pagure_config.get("FEDORA_MESSAGING_NOTIFICATIONS", False):
  59. return
  60. try:
  61. import fedora_messaging.api
  62. import fedora_messaging.exceptions
  63. import pagure_messages
  64. msg_cls = pagure_messages.get_message_object_from_topic(
  65. "pagure.{}".format(topic)
  66. )
  67. if not hasattr(msg_cls, "app_name") is False:
  68. _log.warning(
  69. "pagure is about to send a message that has no schemas: %s",
  70. topic,
  71. )
  72. msg = msg_cls(body=message)
  73. if not msg.topic:
  74. msg.topic = "pagure.{}".format(topic)
  75. fedora_messaging.api.publish(msg)
  76. except ImportError:
  77. _log.warning(
  78. "Fedora messaging or pagure-messages does not appear to be "
  79. "available"
  80. )
  81. except fedora_messaging.exceptions.PublishReturned as e:
  82. _log.warning(
  83. "Fedora Messaging broker rejected message %s: %s", msg.id, e
  84. )
  85. except fedora_messaging.exceptions.ConnectionException as e:
  86. _log.warning("Error sending message %s: %s", msg.id, e)
  87. except Exception:
  88. _log.exception("Error sending fedora-messaging message")
  89. stomp_conn = None
  90. def stomp_publish(topic, message):
  91. """ Try to publish a message on a Stomp-compliant message bus. """
  92. if not pagure_config.get("STOMP_NOTIFICATIONS", False):
  93. return
  94. # We catch Exception if we want :-p
  95. # pylint: disable=broad-except
  96. # Ignore message about stomp import
  97. # pylint: disable=import-error
  98. try:
  99. import stomp
  100. global stomp_conn
  101. if not stomp_conn or not stomp_conn.is_connected():
  102. stomp_conn = stomp.Connection12(pagure_config["STOMP_BROKERS"])
  103. if pagure_config.get("STOMP_SSL"):
  104. stomp_conn.set_ssl(
  105. pagure_config["STOMP_BROKERS"],
  106. key_file=pagure_config.get("STOMP_KEY_FILE"),
  107. cert_file=pagure_config.get("STOMP_CERT_FILE"),
  108. password=pagure_config.get("STOMP_CREDS_PASSWORD"),
  109. )
  110. from stomp import PrintingListener
  111. stomp_conn.set_listener("", PrintingListener())
  112. stomp_conn.start()
  113. stomp_conn.connect(wait=True)
  114. hierarchy = pagure_config["STOMP_HIERARCHY"]
  115. stomp_conn.send(
  116. destination=hierarchy + topic, body=json.dumps(message)
  117. )
  118. except Exception:
  119. _log.exception("Error sending stomp message")
  120. def blinker_publish(topic, message):
  121. _log.info("Sending blinker signal to: pagure - topic: %s", topic)
  122. ready = blinker.signal("pagure")
  123. ready.send("pagure", topic=topic, message=message)
  124. def mqtt_publish(topic, message):
  125. """ Try to publish a message on a MQTT message bus. """
  126. if not pagure_config.get("MQTT_NOTIFICATIONS", False):
  127. return
  128. mqtt_host = pagure_config.get("MQTT_HOST")
  129. mqtt_port = pagure_config.get("MQTT_PORT")
  130. mqtt_username = pagure_config.get("MQTT_USERNAME")
  131. mqtt_pass = pagure_config.get("MQTT_PASSWORD")
  132. mqtt_ca_certs = pagure_config.get("MQTT_CA_CERTS")
  133. mqtt_certfile = pagure_config.get("MQTT_CERTFILE")
  134. mqtt_keyfile = pagure_config.get("MQTT_KEYFILE")
  135. mqtt_cert_reqs = pagure_config.get("MQTT_CERT_REQS", ssl.CERT_REQUIRED)
  136. mqtt_tls_version = pagure_config.get(
  137. "MQTT_TLS_VERSION", ssl.PROTOCOL_TLSv1_2
  138. )
  139. mqtt_ciphers = pagure_config.get("MQTT_CIPHERS")
  140. mqtt_topic_prefix = pagure_config.get("MQTT_TOPIC_PREFIX") or None
  141. if mqtt_topic_prefix:
  142. topic = "/".join([mqtt_topic_prefix.rstrip("/"), topic])
  143. # We catch Exception if we want :-p
  144. # pylint: disable=broad-except
  145. # Ignore message about mqtt import
  146. # pylint: disable=import-error
  147. try:
  148. import paho.mqtt.client as mqtt
  149. client = mqtt.Client(os.uname()[1])
  150. client.tls_set(
  151. ca_certs=mqtt_ca_certs,
  152. certfile=mqtt_certfile,
  153. keyfile=mqtt_keyfile,
  154. cert_reqs=mqtt_cert_reqs,
  155. tls_version=mqtt_tls_version,
  156. ciphers=mqtt_ciphers,
  157. )
  158. if mqtt_username and mqtt_pass:
  159. client.username_pw_set(mqtt_username, mqtt_pass)
  160. client.connect(mqtt_host, mqtt_port)
  161. client.publish(topic, json.dumps(message))
  162. client.disconnect()
  163. except Exception:
  164. _log.exception("Error sending mqtt message")
  165. def log(project, topic, msg, webhook=True):
  166. """This is the place where we send notifications to user about actions
  167. occuring in pagure.
  168. """
  169. # Send fedmsg notification (if fedmsg is there and set-up)
  170. if not project or (
  171. project.settings.get("fedmsg_notifications", True)
  172. and not project.private
  173. ):
  174. fedmsg_publish(topic, msg)
  175. fedora_messaging_publish(topic, msg)
  176. # Send stomp notification (if stomp is there and set-up)
  177. if not project or (
  178. project.settings.get("stomp_notifications", True)
  179. and not project.private
  180. ):
  181. stomp_publish(topic, msg)
  182. # Send mqtt notification (if mqtt is there and set-up)
  183. if not project or (
  184. project.settings.get("mqtt_notifications", True)
  185. and not project.private
  186. ):
  187. mqtt_publish(topic, msg)
  188. # Send blink notification to any 3rd party plugins, if there are any
  189. blinker_publish(topic, msg)
  190. if webhook and project and not project.private:
  191. pagure.lib.tasks_services.webhook_notification.delay(
  192. topic=topic,
  193. msg=msg,
  194. namespace=project.namespace,
  195. name=project.name,
  196. user=project.user.username if project.is_fork else None,
  197. )
  198. def _add_mentioned_users(emails, comment):
  199. """Check the comment to see if an user is mentioned in it and if
  200. so add this user to the list of people to notify.
  201. """
  202. filtered_comment = re.sub(
  203. FencedBlockPreprocessor.FENCED_BLOCK_RE, "", comment
  204. )
  205. for username in re.findall(MENTION_RE, filtered_comment):
  206. user = pagure.lib.query.search_user(flask.g.session, username=username)
  207. if user:
  208. emails.add(user.default_email)
  209. return emails
  210. def _clean_emails(emails, user):
  211. """Remove the email of the user doing the action if it is in the list.
  212. This avoids receiving emails about action you do.
  213. """
  214. # Remove the user doing the action from the list of person to email
  215. # unless they actively asked for it
  216. if (
  217. user
  218. and user.emails
  219. and not user.settings.get("cc_me_to_my_actions", False)
  220. ):
  221. for email in user.emails:
  222. if email.email in emails:
  223. emails.remove(email.email)
  224. return emails
  225. def _get_emails_for_obj(obj):
  226. """Return the list of emails to send notification to when notifying
  227. about the specified issue or pull-request.
  228. """
  229. emails = set()
  230. # Add project creator/owner
  231. if obj.project.user.default_email:
  232. emails.add(obj.project.user.default_email)
  233. # Add committers is object is private, otherwise all contributors
  234. if obj.isa in ["issue", "pull-request"] and obj.private:
  235. for user in obj.project.committers:
  236. if user.default_email:
  237. emails.add(user.default_email)
  238. else:
  239. for user in obj.project.users:
  240. if user.default_email:
  241. emails.add(user.default_email)
  242. # Add people in groups with any access to the project:
  243. for group in obj.project.groups:
  244. if group.creator.default_email:
  245. emails.add(group.creator.default_email)
  246. for user in group.users:
  247. if user.default_email:
  248. emails.add(user.default_email)
  249. # Add people that commented on the issue/PR
  250. if obj.isa in ["issue", "pull-request"]:
  251. for comment in obj.comments:
  252. if comment.user.default_email:
  253. emails.add(comment.user.default_email)
  254. # Add public notifications to lists/users set project-wide
  255. if obj.isa == "issue" and not obj.private:
  256. for notifs in obj.project.notifications.get("issues", []):
  257. emails.add(notifs)
  258. elif obj.isa == "pull-request":
  259. for notifs in obj.project.notifications.get("requests", []):
  260. emails.add(notifs)
  261. # Add the person watching this project, if it's a public issue or a
  262. # pull-request
  263. if (obj.isa == "issue" and not obj.private) or obj.isa == "pull-request":
  264. for watcher in obj.project.watchers:
  265. if watcher.watch_issues:
  266. emails.add(watcher.user.default_email)
  267. else:
  268. # If there is a watch entry and it is false, it means the user
  269. # explicitly requested to not watch the issue
  270. if watcher.user.default_email in emails:
  271. emails.remove(watcher.user.default_email)
  272. # Add/Remove people who explicitly asked to be added/removed
  273. if obj.isa in ["issue", "pull-request"]:
  274. for watcher in obj.watchers:
  275. if not watcher.watch and watcher.user.default_email in emails:
  276. emails.remove(watcher.user.default_email)
  277. elif watcher.watch:
  278. emails.add(watcher.user.default_email)
  279. # Drop the email used by pagure when sending
  280. emails = _clean_emails(
  281. emails,
  282. pagure_config.get(
  283. pagure_config.get("FROM_EMAIL", "pagure@fedoraproject.org")
  284. ),
  285. )
  286. # Add the person that opened the issue/PR
  287. if obj.user.default_email:
  288. emails.add(obj.user.default_email)
  289. # Add the person assigned to the issue/PR
  290. if obj.isa in ["issue", "pull-request"]:
  291. if obj.assignee and obj.assignee.default_email:
  292. emails.add(obj.assignee.default_email)
  293. return emails
  294. def _get_emails_for_commit_notification(project):
  295. emails = set()
  296. for watcher in project.watchers:
  297. if watcher.watch_commits:
  298. emails.add(watcher.user.default_email)
  299. # Drop the email used by pagure when sending
  300. emails = _clean_emails(
  301. emails,
  302. pagure_config.get(
  303. pagure_config.get("FROM_EMAIL", "pagure@fedoraproject.org")
  304. ),
  305. )
  306. return emails
  307. def _build_url(*args):
  308. """ Build a URL from a given list of arguments. """
  309. items = []
  310. for idx, arg in enumerate(args):
  311. arg = "%s" % arg
  312. if arg.startswith("/"):
  313. arg = arg[1:]
  314. if arg.endswith("/") and not idx + 1 == len(args):
  315. arg = arg[:-1]
  316. items.append(arg)
  317. return "/".join(items)
  318. def _fullname_to_url(fullname):
  319. """For forked projects, fullname is 'forks/user/...' but URL is
  320. 'fork/user/...'. This is why we can't have nice things.
  321. """
  322. if fullname.startswith("forks/"):
  323. fullname = fullname.replace("forks", "fork", 1)
  324. return fullname
  325. def send_email(
  326. text,
  327. subject,
  328. to_mail,
  329. mail_id=None,
  330. in_reply_to=None,
  331. project_name=None,
  332. user_from=None,
  333. reporter=None,
  334. assignee=None,
  335. ): # pragma: no cover
  336. """Send an email with the specified information.
  337. :arg text: the content of the email to send
  338. :type text: unicode
  339. :arg subject: the subject of the email
  340. :arg to_mail: a string representing a list of recipient separated by a
  341. comma
  342. :kwarg mail_id: if defined, the header `mail-id` is set with this value
  343. :kwarg in_reply_to: if defined, the header `In-Reply-To` is set with
  344. this value
  345. :kwarg project_name: if defined, the name of the project
  346. """
  347. if not to_mail:
  348. return
  349. from_email = pagure_config.get("FROM_EMAIL", "pagure@fedoraproject.org")
  350. if isinstance(from_email, bytes):
  351. from_email = from_email.decode("utf-8")
  352. if user_from:
  353. header = Header(user_from, "utf-8")
  354. from_email = "%s <%s>" % (header.encode(), from_email)
  355. if project_name is not None:
  356. subject_tag = project_name
  357. else:
  358. subject_tag = "Pagure"
  359. if mail_id:
  360. mail_id = mail_id + "@%s" % pagure_config["DOMAIN_EMAIL_NOTIFICATIONS"]
  361. if in_reply_to:
  362. in_reply_to = (
  363. in_reply_to + "@%s" % pagure_config["DOMAIN_EMAIL_NOTIFICATIONS"]
  364. )
  365. smtp = None
  366. for mailto in to_mail.split(","):
  367. try:
  368. pagure.lib.query.allowed_emailaddress(mailto)
  369. except pagure.exceptions.PagureException:
  370. continue
  371. msg = MIMEText(text.encode("utf-8"), "plain", "utf-8")
  372. msg["Subject"] = Header("[%s] %s" % (subject_tag, subject), "utf-8")
  373. msg["From"] = from_email
  374. if mail_id:
  375. msg["mail-id"] = mail_id
  376. msg["Message-Id"] = "<%s>" % mail_id
  377. if in_reply_to:
  378. msg["In-Reply-To"] = "<%s>" % in_reply_to
  379. msg["X-Auto-Response-Suppress"] = "All"
  380. msg["X-pagure"] = pagure_config["APP_URL"]
  381. if project_name is not None:
  382. msg["X-pagure-project"] = project_name
  383. msg["List-ID"] = project_name
  384. msg["List-Archive"] = _build_url(
  385. pagure_config["APP_URL"], _fullname_to_url(project_name)
  386. )
  387. if reporter is not None:
  388. msg["X-pagure-reporter"] = reporter
  389. if assignee is not None:
  390. msg["X-pagure-assignee"] = assignee
  391. # Send the message via our own SMTP server, but don't include the
  392. # envelope header.
  393. msg["To"] = mailto
  394. salt = pagure_config.get("SALT_EMAIL")
  395. if salt and not isinstance(salt, bytes):
  396. salt = salt.encode("utf-8")
  397. if mail_id and pagure_config["EVENTSOURCE_SOURCE"]:
  398. key = (
  399. b"<"
  400. + mail_id.encode("utf-8")
  401. + b">"
  402. + salt
  403. + mailto.encode("utf-8")
  404. )
  405. if isinstance(key, six.text_type):
  406. key = key.encode("utf-8")
  407. mhash = hashlib.sha512(key)
  408. msg["Reply-To"] = "reply+%s@%s" % (
  409. mhash.hexdigest(),
  410. pagure_config["DOMAIN_EMAIL_NOTIFICATIONS"],
  411. )
  412. msg["Mail-Followup-To"] = msg["Reply-To"]
  413. if not pagure_config.get("EMAIL_SEND", True):
  414. _log.debug("******EMAIL******")
  415. _log.debug("From: %s", from_email)
  416. _log.debug("To: %s", to_mail)
  417. _log.debug("Subject: %s", subject)
  418. _log.debug("in_reply_to: %s", in_reply_to)
  419. _log.debug("mail_id: %s", mail_id)
  420. _log.debug("Contents:")
  421. _log.debug("%s" % text)
  422. _log.debug("*****************")
  423. _log.debug(msg.as_string())
  424. _log.debug("*****/EMAIL******")
  425. continue
  426. try:
  427. if smtp is None:
  428. if pagure_config["SMTP_SSL"]:
  429. smtp = smtplib.SMTP_SSL(
  430. pagure_config["SMTP_SERVER"],
  431. pagure_config["SMTP_PORT"],
  432. )
  433. else:
  434. smtp = smtplib.SMTP(
  435. pagure_config["SMTP_SERVER"],
  436. pagure_config["SMTP_PORT"],
  437. )
  438. if pagure_config.get("SMTP_STARTTLS"):
  439. context = ssl.create_default_context()
  440. keyfile = pagure_config.get("SMTP_KEYFILE") or None
  441. certfile = pagure_config.get("SMTP_CERTFILE") or None
  442. respcode, _ = smtp.starttls(
  443. keyfile=keyfile,
  444. certfile=certfile,
  445. context=context,
  446. )
  447. if respcode != 220:
  448. _log.warning(
  449. "The starttls command did not return the 220 "
  450. "response code expected."
  451. )
  452. if (
  453. pagure_config["SMTP_USERNAME"]
  454. and pagure_config["SMTP_PASSWORD"]
  455. ):
  456. smtp.login(
  457. pagure_config["SMTP_USERNAME"],
  458. pagure_config["SMTP_PASSWORD"],
  459. )
  460. smtp.sendmail(from_email, [mailto], msg.as_string())
  461. except smtplib.SMTPException as err:
  462. _log.exception(err)
  463. if smtp:
  464. smtp.quit()
  465. return msg
  466. def notify_new_comment(comment, user=None):
  467. """Notify the people following an issue that a new comment was added
  468. to the issue.
  469. """
  470. text = """
  471. %s added a new comment to an issue you are following:
  472. ``
  473. %s
  474. ``
  475. %s
  476. %s
  477. """ % (
  478. comment.user.user,
  479. comment.comment,
  480. REPLY_MSG,
  481. _build_url(
  482. pagure_config["APP_URL"],
  483. _fullname_to_url(comment.issue.project.fullname),
  484. "issue",
  485. comment.issue.id,
  486. ),
  487. )
  488. mail_to = _get_emails_for_obj(comment.issue)
  489. if comment.user and comment.user.default_email:
  490. mail_to.add(comment.user.default_email)
  491. mail_to = _add_mentioned_users(mail_to, comment.comment)
  492. mail_to = _clean_emails(mail_to, user)
  493. assignee = comment.issue.assignee.user if comment.issue.assignee else None
  494. send_email(
  495. text,
  496. "Issue #%s: %s" % (comment.issue.id, comment.issue.title),
  497. ",".join(mail_to),
  498. mail_id=comment.mail_id,
  499. in_reply_to=comment.issue.mail_id,
  500. project_name=comment.issue.project.fullname,
  501. user_from=comment.user.fullname or comment.user.user,
  502. reporter=comment.issue.user.user,
  503. assignee=assignee,
  504. )
  505. def notify_new_issue(issue, user=None):
  506. """Notify the people following a project that a new issue was added
  507. to it.
  508. """
  509. text = """
  510. %s reported a new issue against the project: `%s` that you are following:
  511. ``
  512. %s
  513. ``
  514. %s
  515. %s
  516. """ % (
  517. issue.user.user,
  518. issue.project.name,
  519. issue.content,
  520. REPLY_MSG,
  521. _build_url(
  522. pagure_config["APP_URL"],
  523. _fullname_to_url(issue.project.fullname),
  524. "issue",
  525. issue.id,
  526. ),
  527. )
  528. mail_to = _get_emails_for_obj(issue)
  529. mail_to = _add_mentioned_users(mail_to, issue.content)
  530. mail_to = _clean_emails(mail_to, user)
  531. assignee = issue.assignee.user if issue.assignee else None
  532. send_email(
  533. text,
  534. "Issue #%s: %s" % (issue.id, issue.title),
  535. ",".join(mail_to),
  536. mail_id=issue.mail_id,
  537. project_name=issue.project.fullname,
  538. user_from=issue.user.fullname or issue.user.user,
  539. reporter=issue.user.user,
  540. assignee=assignee,
  541. )
  542. def notify_assigned_issue(issue, new_assignee, user):
  543. """Notify the people following an issue that the assignee changed."""
  544. action = "reset"
  545. if new_assignee:
  546. action = "assigned to `%s`" % new_assignee.user
  547. text = """
  548. The issue: `%s` of project: `%s` has been %s by %s.
  549. %s
  550. """ % (
  551. issue.title,
  552. issue.project.name,
  553. action,
  554. user.username,
  555. _build_url(
  556. pagure_config["APP_URL"],
  557. _fullname_to_url(issue.project.fullname),
  558. "issue",
  559. issue.id,
  560. ),
  561. )
  562. mail_to = _get_emails_for_obj(issue)
  563. if new_assignee and new_assignee.default_email:
  564. mail_to.add(new_assignee.default_email)
  565. mail_to = _clean_emails(mail_to, user)
  566. uid = time.mktime(datetime.datetime.now().timetuple())
  567. assignee = issue.assignee.user if issue.assignee else None
  568. send_email(
  569. text,
  570. "Issue #%s: %s" % (issue.id, issue.title),
  571. ",".join(mail_to),
  572. mail_id="%s/assigned/%s" % (issue.mail_id, uid),
  573. in_reply_to=issue.mail_id,
  574. project_name=issue.project.fullname,
  575. user_from=user.fullname or user.user,
  576. reporter=issue.user.user,
  577. assignee=assignee,
  578. )
  579. def notify_status_change_issue(issue, user):
  580. """Notify the people following a project that an issue changed status."""
  581. status = issue.status
  582. if status.lower() != "open" and issue.close_status:
  583. status = "%s as %s" % (status, issue.close_status)
  584. text = """
  585. The status of the issue: `%s` of project: `%s` has been updated to: %s by %s.
  586. %s
  587. """ % (
  588. issue.title,
  589. issue.project.fullname,
  590. status,
  591. user.username,
  592. _build_url(
  593. pagure_config["APP_URL"],
  594. _fullname_to_url(issue.project.fullname),
  595. "issue",
  596. issue.id,
  597. ),
  598. )
  599. mail_to = _get_emails_for_obj(issue)
  600. uid = time.mktime(datetime.datetime.now().timetuple())
  601. assignee = issue.assignee.user if issue.assignee else None
  602. send_email(
  603. text,
  604. "Issue #%s: %s" % (issue.id, issue.title),
  605. ",".join(mail_to),
  606. mail_id="%s/close/%s" % (issue.mail_id, uid),
  607. in_reply_to=issue.mail_id,
  608. project_name=issue.project.fullname,
  609. user_from=user.fullname or user.user,
  610. reporter=issue.user.user,
  611. assignee=assignee,
  612. )
  613. def notify_meta_change_issue(issue, user, msg):
  614. """Notify that a custom field changed"""
  615. text = """
  616. `%s` updated issue.
  617. %s
  618. %s
  619. """ % (
  620. user.username,
  621. msg,
  622. _build_url(
  623. pagure_config["APP_URL"],
  624. _fullname_to_url(issue.project.fullname),
  625. "issue",
  626. issue.id,
  627. ),
  628. )
  629. mail_to = _get_emails_for_obj(issue)
  630. uid = time.mktime(datetime.datetime.now().timetuple())
  631. assignee = issue.assignee.user if issue.assignee else None
  632. send_email(
  633. text,
  634. "Issue #%s: %s" % (issue.id, issue.title),
  635. ",".join(mail_to),
  636. mail_id="%s/close/%s" % (issue.mail_id, uid),
  637. in_reply_to=issue.mail_id,
  638. project_name=issue.project.fullname,
  639. user_from=user.fullname or user.user,
  640. reporter=issue.user.user,
  641. assignee=assignee,
  642. )
  643. def notify_assigned_request(request, new_assignee, user):
  644. """Notify the people following a pull-request that the assignee changed."""
  645. action = "reset"
  646. if new_assignee:
  647. action = "assigned to `%s`" % new_assignee.user
  648. text = """
  649. The pull-request: `%s` of project: `%s` has been %s by %s.
  650. %s
  651. """ % (
  652. request.title,
  653. request.project.name,
  654. action,
  655. user.username,
  656. _build_url(
  657. pagure_config["APP_URL"],
  658. _fullname_to_url(request.project.fullname),
  659. "pull-request",
  660. request.id,
  661. ),
  662. )
  663. mail_to = _get_emails_for_obj(request)
  664. if new_assignee and new_assignee.default_email:
  665. mail_to.add(new_assignee.default_email)
  666. mail_to = _clean_emails(mail_to, user)
  667. uid = time.mktime(datetime.datetime.now().timetuple())
  668. assignee = request.assignee.user if request.assignee else None
  669. send_email(
  670. text,
  671. "PR #%s: %s" % (request.id, request.title),
  672. ",".join(mail_to),
  673. mail_id="%s/assigned/%s" % (request.mail_id, uid),
  674. in_reply_to=request.mail_id,
  675. project_name=request.project.fullname,
  676. user_from=user.fullname or user.user,
  677. reporter=request.user.user,
  678. assignee=assignee,
  679. )
  680. def notify_new_pull_request(request):
  681. """Notify the people following a project that a new pull-request was
  682. added to it.
  683. """
  684. text = """
  685. %s opened a new pull-request against the project: `%s` that you are following:
  686. ``
  687. %s
  688. ``
  689. %s
  690. %s
  691. """ % (
  692. request.user.user,
  693. request.project.name,
  694. request.title,
  695. REPLY_MSG,
  696. _build_url(
  697. pagure_config["APP_URL"],
  698. _fullname_to_url(request.project.fullname),
  699. "pull-request",
  700. request.id,
  701. ),
  702. )
  703. mail_to = _get_emails_for_obj(request)
  704. assignee = request.assignee.user if request.assignee else None
  705. send_email(
  706. text,
  707. "PR #%s: %s" % (request.id, request.title),
  708. ",".join(mail_to),
  709. mail_id=request.mail_id,
  710. project_name=request.project.fullname,
  711. user_from=request.user.fullname or request.user.user,
  712. reporter=request.user.user,
  713. assignee=assignee,
  714. )
  715. def notify_merge_pull_request(request, user):
  716. """Notify the people following a project that a pull-request was merged
  717. in it.
  718. """
  719. text = """
  720. %s merged a pull-request against the project: `%s` that you are following.
  721. Merged pull-request:
  722. ``
  723. %s
  724. ``
  725. %s
  726. """ % (
  727. user.username,
  728. request.project.name,
  729. request.title,
  730. _build_url(
  731. pagure_config["APP_URL"],
  732. _fullname_to_url(request.project.fullname),
  733. "pull-request",
  734. request.id,
  735. ),
  736. )
  737. mail_to = _get_emails_for_obj(request)
  738. uid = time.mktime(datetime.datetime.now().timetuple())
  739. assignee = request.assignee.user if request.assignee else None
  740. send_email(
  741. text,
  742. "PR #%s: %s" % (request.id, request.title),
  743. ",".join(mail_to),
  744. mail_id="%s/close/%s" % (request.mail_id, uid),
  745. in_reply_to=request.mail_id,
  746. project_name=request.project.fullname,
  747. user_from=user.fullname or user.user,
  748. reporter=request.user.user,
  749. assignee=assignee,
  750. )
  751. def notify_reopen_pull_request(request, user):
  752. """Notify the people following a project that a closed pull-request
  753. has been reopened.
  754. """
  755. text = """
  756. %s reopened a pull-request against the project: `%s` that you are following.
  757. Reopened pull-request:
  758. ``
  759. %s
  760. ``
  761. %s
  762. """ % (
  763. user.username,
  764. request.project.name,
  765. request.title,
  766. _build_url(
  767. pagure_config["APP_URL"],
  768. _fullname_to_url(request.project.fullname),
  769. "pull-request",
  770. request.id,
  771. ),
  772. )
  773. mail_to = _get_emails_for_obj(request)
  774. uid = time.mktime(datetime.datetime.now().timetuple())
  775. assignee = request.assignee.user if request.assignee else None
  776. send_email(
  777. text,
  778. "PR #%s: %s" % (request.id, request.title),
  779. ",".join(mail_to),
  780. mail_id="%s/close/%s" % (request.mail_id, uid),
  781. in_reply_to=request.mail_id,
  782. project_name=request.project.fullname,
  783. user_from=user.fullname or user.user,
  784. reporter=request.user.user,
  785. assignee=assignee,
  786. )
  787. def notify_closed_pull_request(request, user):
  788. """Notify the people following a project that a pull-request was
  789. closed in it.
  790. """
  791. text = """
  792. %s closed without merging a pull-request against the project: `%s` that you
  793. are following.
  794. Closed pull-request:
  795. ``
  796. %s
  797. ``
  798. %s
  799. """ % (
  800. user.username,
  801. request.project.name,
  802. request.title,
  803. _build_url(
  804. pagure_config["APP_URL"],
  805. _fullname_to_url(request.project.fullname),
  806. "pull-request",
  807. request.id,
  808. ),
  809. )
  810. mail_to = _get_emails_for_obj(request)
  811. uid = time.mktime(datetime.datetime.now().timetuple())
  812. assignee = request.assignee.user if request.assignee else None
  813. send_email(
  814. text,
  815. "PR #%s: %s" % (request.id, request.title),
  816. ",".join(mail_to),
  817. mail_id="%s/close/%s" % (request.mail_id, uid),
  818. in_reply_to=request.mail_id,
  819. project_name=request.project.fullname,
  820. user_from=user.fullname or user.user,
  821. reporter=request.user.user,
  822. assignee=assignee,
  823. )
  824. def notify_pull_request_comment(comment, user):
  825. """Notify the people following a pull-request that a new comment was
  826. added to it.
  827. """
  828. text = """
  829. %s commented on the pull-request: `%s` that you are following:
  830. ``
  831. %s
  832. ``
  833. %s
  834. %s
  835. """ % (
  836. comment.user.user,
  837. comment.pull_request.title,
  838. comment.comment,
  839. REPLY_MSG,
  840. _build_url(
  841. pagure_config["APP_URL"],
  842. _fullname_to_url(comment.pull_request.project.fullname),
  843. "pull-request",
  844. comment.pull_request.id,
  845. ),
  846. )
  847. mail_to = _get_emails_for_obj(comment.pull_request)
  848. mail_to = _add_mentioned_users(mail_to, comment.comment)
  849. mail_to = _clean_emails(mail_to, user)
  850. assignee = (
  851. comment.pull_request.assignee.user
  852. if comment.pull_request.assignee
  853. else None
  854. )
  855. send_email(
  856. text,
  857. "PR #%s: %s" % (comment.pull_request.id, comment.pull_request.title),
  858. ",".join(mail_to),
  859. mail_id=comment.mail_id,
  860. in_reply_to=comment.pull_request.mail_id,
  861. project_name=comment.pull_request.project.fullname,
  862. user_from=comment.user.fullname or comment.user.user,
  863. reporter=comment.pull_request.user.user,
  864. assignee=assignee,
  865. )
  866. def notify_pull_request_flag(flag, request, user):
  867. """Notify the people following a pull-request that a new flag was
  868. added to it.
  869. """
  870. text = """
  871. %s flagged the pull-request `%s` as %s: %s
  872. %s
  873. """ % (
  874. flag.username,
  875. request.title,
  876. flag.status,
  877. flag.comment,
  878. _build_url(
  879. pagure_config["APP_URL"],
  880. _fullname_to_url(request.project.fullname),
  881. "pull-request",
  882. request.id,
  883. ),
  884. )
  885. mail_to = _get_emails_for_obj(request)
  886. assignee = request.assignee.user if request.assignee else None
  887. send_email(
  888. text,
  889. "PR #%s - %s: %s" % (request.id, flag.username, flag.status),
  890. ",".join(mail_to),
  891. mail_id=flag.mail_id,
  892. in_reply_to=request.mail_id,
  893. project_name=request.project.fullname,
  894. user_from=flag.username,
  895. reporter=request.user.user,
  896. assignee=assignee,
  897. )
  898. def notify_new_email(email, user):
  899. """Ask the user to confirm to the email belong to them."""
  900. root_url = pagure_config.get("APP_URL", flask.request.url_root)
  901. url = urljoin(
  902. root_url or flask.request.url_root,
  903. flask.url_for("ui_ns.confirm_email", token=email.token),
  904. )
  905. text = """Dear %(username)s,
  906. You have registered a new email on pagure at %(root_url)s.
  907. To finish your validate this registration, please click on the following
  908. link or copy/paste it in your browser, this link will remain valid only 2 days:
  909. %(url)s
  910. The email will not be activated until you finish this step.
  911. Sincerely,
  912. Your pagure admin.
  913. """ % (
  914. {"username": user.username, "url": url, "root_url": root_url}
  915. )
  916. send_email(
  917. text,
  918. "Confirm new email",
  919. email.email,
  920. user_from=user.fullname or user.user,
  921. )
  922. def notify_new_commits(abspath, project, branch, commits):
  923. """Notify the people following a project's commits that new commits have
  924. been added.
  925. """
  926. # string note: abspath, project and branch can only contain ASCII
  927. # by policy (pagure and/or gitolite)
  928. commits_info = []
  929. for commit in commits:
  930. commits_info.append(
  931. {
  932. "commit": commit,
  933. "author": pagure.lib.git.get_author(commit, abspath),
  934. "subject": pagure.lib.git.get_commit_subject(commit, abspath),
  935. }
  936. )
  937. # make sure this is unicode
  938. commits_string = "\n".join(
  939. "{0} {1} {2}".format(
  940. commit_info["commit"],
  941. commit_info["author"],
  942. commit_info["subject"],
  943. )
  944. for commit_info in commits_info
  945. )
  946. commit_url = _build_url(
  947. pagure_config["APP_URL"],
  948. _fullname_to_url(project.fullname),
  949. "commits",
  950. branch,
  951. )
  952. email_body = """
  953. The following commits were pushed to the repo %s on branch
  954. %s, which you are following:
  955. %s
  956. To view more about the commits, visit:
  957. %s
  958. """ % (
  959. project.fullname,
  960. branch,
  961. commits_string,
  962. commit_url,
  963. )
  964. mail_to = _get_emails_for_commit_notification(project)
  965. send_email(
  966. email_body,
  967. 'New Commits To "{0}" ({1})'.format(project.fullname, branch),
  968. ",".join(mail_to),
  969. project_name=project.fullname,
  970. )
  971. def notify_commit_flag(flag, user):
  972. """Notify the people following a project that a new flag was added
  973. to one of its commit.
  974. """
  975. text = """
  976. %s flagged the commit `%s` as %s: %s
  977. %s
  978. """ % (
  979. flag.username,
  980. flag.commit_hash,
  981. flag.status,
  982. flag.comment,
  983. _build_url(
  984. pagure_config["APP_URL"],
  985. _fullname_to_url(flag.project.fullname),
  986. "c",
  987. flag.commit_hash,
  988. ),
  989. )
  990. mail_to = _get_emails_for_obj(flag)
  991. send_email(
  992. text,
  993. "Commit #%s - %s: %s" % (flag.commit_hash, flag.username, flag.status),
  994. ",".join(mail_to),
  995. mail_id=flag.mail_id,
  996. in_reply_to=flag.project.mail_id,
  997. project_name=flag.project.fullname,
  998. user_from=flag.username,
  999. )