alias.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. from flask import g
  2. from flask import jsonify
  3. from flask import request
  4. from flask_cors import cross_origin
  5. from app import alias_utils
  6. from app.api.base import api_bp, require_api_auth
  7. from app.api.serializer import (
  8. AliasInfo,
  9. serialize_alias_info,
  10. serialize_contact,
  11. get_alias_infos_with_pagination,
  12. get_alias_contacts,
  13. get_alias_infos_with_pagination_v2,
  14. serialize_alias_info_v2,
  15. get_alias_info_v2,
  16. )
  17. from app.config import EMAIL_DOMAIN
  18. from app.dashboard.views.alias_log import get_alias_log
  19. from app.email_utils import parseaddr_unicode
  20. from app.extensions import db
  21. from app.log import LOG
  22. from app.models import Alias, Contact, Mailbox, AliasMailbox
  23. from app.utils import random_string
  24. @api_bp.route("/aliases", methods=["GET", "POST"])
  25. @cross_origin()
  26. @require_api_auth
  27. def get_aliases():
  28. """
  29. Get aliases
  30. Input:
  31. page_id: in query
  32. Output:
  33. - aliases: list of alias:
  34. - id
  35. - email
  36. - creation_date
  37. - creation_timestamp
  38. - nb_forward
  39. - nb_block
  40. - nb_reply
  41. - note
  42. """
  43. user = g.user
  44. try:
  45. page_id = int(request.args.get("page_id"))
  46. except (ValueError, TypeError):
  47. return jsonify(error="page_id must be provided in request query"), 400
  48. query = None
  49. data = request.get_json(silent=True)
  50. if data:
  51. query = data.get("query")
  52. alias_infos: [AliasInfo] = get_alias_infos_with_pagination(
  53. user, page_id=page_id, query=query
  54. )
  55. return (
  56. jsonify(
  57. aliases=[serialize_alias_info(alias_info) for alias_info in alias_infos]
  58. ),
  59. 200,
  60. )
  61. @api_bp.route("/v2/aliases", methods=["GET", "POST"])
  62. @cross_origin()
  63. @require_api_auth
  64. def get_aliases_v2():
  65. """
  66. Get aliases
  67. Input:
  68. page_id: in query
  69. Output:
  70. - aliases: list of alias:
  71. - id
  72. - email
  73. - creation_date
  74. - creation_timestamp
  75. - nb_forward
  76. - nb_block
  77. - nb_reply
  78. - note
  79. - mailbox
  80. - mailboxes
  81. - (optional) latest_activity:
  82. - timestamp
  83. - action: forward|reply|block|bounced
  84. - contact:
  85. - email
  86. - name
  87. - reverse_alias
  88. """
  89. user = g.user
  90. try:
  91. page_id = int(request.args.get("page_id"))
  92. except (ValueError, TypeError):
  93. return jsonify(error="page_id must be provided in request query"), 400
  94. query = None
  95. data = request.get_json(silent=True)
  96. if data:
  97. query = data.get("query")
  98. alias_infos: [AliasInfo] = get_alias_infos_with_pagination_v2(
  99. user, page_id=page_id, query=query
  100. )
  101. return (
  102. jsonify(
  103. aliases=[serialize_alias_info_v2(alias_info) for alias_info in alias_infos]
  104. ),
  105. 200,
  106. )
  107. @api_bp.route("/aliases/<int:alias_id>", methods=["DELETE"])
  108. @cross_origin()
  109. @require_api_auth
  110. def delete_alias(alias_id):
  111. """
  112. Delete alias
  113. Input:
  114. alias_id: in url
  115. Output:
  116. 200 if deleted successfully
  117. """
  118. user = g.user
  119. alias = Alias.get(alias_id)
  120. if alias.user_id != user.id:
  121. return jsonify(error="Forbidden"), 403
  122. alias_utils.delete_alias(alias, user)
  123. return jsonify(deleted=True), 200
  124. @api_bp.route("/aliases/<int:alias_id>/toggle", methods=["POST"])
  125. @cross_origin()
  126. @require_api_auth
  127. def toggle_alias(alias_id):
  128. """
  129. Enable/disable alias
  130. Input:
  131. alias_id: in url
  132. Output:
  133. 200 along with new status:
  134. - enabled
  135. """
  136. user = g.user
  137. alias: Alias = Alias.get(alias_id)
  138. if alias.user_id != user.id:
  139. return jsonify(error="Forbidden"), 403
  140. alias.enabled = not alias.enabled
  141. db.session.commit()
  142. return jsonify(enabled=alias.enabled), 200
  143. @api_bp.route("/aliases/<int:alias_id>/activities")
  144. @cross_origin()
  145. @require_api_auth
  146. def get_alias_activities(alias_id):
  147. """
  148. Get aliases
  149. Input:
  150. page_id: in query
  151. Output:
  152. - activities: list of activity:
  153. - from
  154. - to
  155. - timestamp
  156. - action: forward|reply|block|bounced
  157. - reverse_alias
  158. """
  159. user = g.user
  160. try:
  161. page_id = int(request.args.get("page_id"))
  162. except (ValueError, TypeError):
  163. return jsonify(error="page_id must be provided in request query"), 400
  164. alias: Alias = Alias.get(alias_id)
  165. if not alias or alias.user_id != user.id:
  166. return jsonify(error="Forbidden"), 403
  167. alias_logs = get_alias_log(alias, page_id)
  168. activities = []
  169. for alias_log in alias_logs:
  170. activity = {
  171. "timestamp": alias_log.when.timestamp,
  172. "reverse_alias": alias_log.reverse_alias,
  173. }
  174. if alias_log.is_reply:
  175. activity["from"] = alias_log.alias
  176. activity["to"] = alias_log.website_email
  177. activity["action"] = "reply"
  178. else:
  179. activity["to"] = alias_log.alias
  180. activity["from"] = alias_log.website_email
  181. if alias_log.bounced:
  182. activity["action"] = "bounced"
  183. elif alias_log.blocked:
  184. activity["action"] = "block"
  185. else:
  186. activity["action"] = "forward"
  187. activities.append(activity)
  188. return jsonify(activities=activities), 200
  189. @api_bp.route("/aliases/<int:alias_id>", methods=["PUT"])
  190. @cross_origin()
  191. @require_api_auth
  192. def update_alias(alias_id):
  193. """
  194. Update alias note
  195. Input:
  196. alias_id: in url
  197. note (optional): in body
  198. name (optional): in body
  199. mailbox_id (optional): in body
  200. disable_pgp (optional): in body
  201. Output:
  202. 200
  203. """
  204. data = request.get_json()
  205. if not data:
  206. return jsonify(error="request body cannot be empty"), 400
  207. user = g.user
  208. alias: Alias = Alias.get(alias_id)
  209. if alias.user_id != user.id:
  210. return jsonify(error="Forbidden"), 403
  211. changed = False
  212. if "note" in data:
  213. new_note = data.get("note")
  214. alias.note = new_note
  215. changed = True
  216. if "mailbox_id" in data:
  217. mailbox_id = int(data.get("mailbox_id"))
  218. mailbox = Mailbox.get(mailbox_id)
  219. if not mailbox or mailbox.user_id != user.id or not mailbox.verified:
  220. return jsonify(error="Forbidden"), 400
  221. alias.mailbox_id = mailbox_id
  222. changed = True
  223. if "mailbox_ids" in data:
  224. mailbox_ids = [int(m_id) for m_id in data.get("mailbox_ids")]
  225. mailboxes: [Mailbox] = []
  226. # check if all mailboxes belong to user
  227. for mailbox_id in mailbox_ids:
  228. mailbox = Mailbox.get(mailbox_id)
  229. if not mailbox or mailbox.user_id != user.id or not mailbox.verified:
  230. return jsonify(error="Forbidden"), 400
  231. mailboxes.append(mailbox)
  232. if not mailboxes:
  233. return jsonify(error="Must choose at least one mailbox"), 400
  234. # <<< update alias mailboxes >>>
  235. # first remove all existing alias-mailboxes links
  236. AliasMailbox.query.filter_by(alias_id=alias.id).delete()
  237. db.session.flush()
  238. # then add all new mailboxes
  239. for i, mailbox in enumerate(mailboxes):
  240. if i == 0:
  241. alias.mailbox_id = mailboxes[0].id
  242. else:
  243. AliasMailbox.create(alias_id=alias.id, mailbox_id=mailbox.id)
  244. # <<< END update alias mailboxes >>>
  245. changed = True
  246. if "name" in data:
  247. new_name = data.get("name")
  248. alias.name = new_name
  249. changed = True
  250. if "disable_pgp" in data:
  251. alias.disable_pgp = data.get("disable_pgp")
  252. changed = True
  253. if changed:
  254. db.session.commit()
  255. return jsonify(ok=True), 200
  256. @api_bp.route("/aliases/<int:alias_id>", methods=["GET"])
  257. @cross_origin()
  258. @require_api_auth
  259. def get_alias(alias_id):
  260. """
  261. Get alias
  262. Input:
  263. alias_id: in url
  264. Output:
  265. Alias info, same as in get_aliases
  266. """
  267. user = g.user
  268. alias: Alias = Alias.get(alias_id)
  269. if alias.user_id != user.id:
  270. return jsonify(error="Forbidden"), 403
  271. return jsonify(**serialize_alias_info_v2(get_alias_info_v2(alias))), 200
  272. @api_bp.route("/aliases/<int:alias_id>/contacts")
  273. @cross_origin()
  274. @require_api_auth
  275. def get_alias_contacts_route(alias_id):
  276. """
  277. Get alias contacts
  278. Input:
  279. page_id: in query
  280. Output:
  281. - contacts: list of contacts:
  282. - creation_date
  283. - creation_timestamp
  284. - last_email_sent_date
  285. - last_email_sent_timestamp
  286. - contact
  287. - reverse_alias
  288. """
  289. user = g.user
  290. try:
  291. page_id = int(request.args.get("page_id"))
  292. except (ValueError, TypeError):
  293. return jsonify(error="page_id must be provided in request query"), 400
  294. alias: Alias = Alias.get(alias_id)
  295. if alias.user_id != user.id:
  296. return jsonify(error="Forbidden"), 403
  297. contacts = get_alias_contacts(alias, page_id)
  298. return jsonify(contacts=contacts), 200
  299. @api_bp.route("/aliases/<int:alias_id>/contacts", methods=["POST"])
  300. @cross_origin()
  301. @require_api_auth
  302. def create_contact_route(alias_id):
  303. """
  304. Create contact for an alias
  305. Input:
  306. alias_id: in url
  307. contact: in body
  308. Output:
  309. 201 if success
  310. 409 if contact already added
  311. """
  312. data = request.get_json()
  313. if not data:
  314. return jsonify(error="request body cannot be empty"), 400
  315. user = g.user
  316. alias: Alias = Alias.get(alias_id)
  317. if alias.user_id != user.id:
  318. return jsonify(error="Forbidden"), 403
  319. contact_addr = data.get("contact")
  320. # generate a reply_email, make sure it is unique
  321. # not use while to avoid infinite loop
  322. reply_email = f"ra+{random_string(25)}@{EMAIL_DOMAIN}"
  323. for _ in range(1000):
  324. reply_email = f"ra+{random_string(25)}@{EMAIL_DOMAIN}"
  325. if not Contact.get_by(reply_email=reply_email):
  326. break
  327. contact_name, contact_email = parseaddr_unicode(contact_addr)
  328. # already been added
  329. if Contact.get_by(alias_id=alias.id, website_email=contact_email):
  330. return jsonify(error="Contact already added"), 409
  331. contact = Contact.create(
  332. user_id=alias.user_id,
  333. alias_id=alias.id,
  334. website_email=contact_email,
  335. name=contact_name,
  336. reply_email=reply_email,
  337. )
  338. LOG.d("create reverse-alias for %s %s", contact_addr, alias)
  339. db.session.commit()
  340. return jsonify(**serialize_contact(contact)), 201
  341. @api_bp.route("/contacts/<int:contact_id>", methods=["DELETE"])
  342. @cross_origin()
  343. @require_api_auth
  344. def delete_contact(contact_id):
  345. """
  346. Delete contact
  347. Input:
  348. contact_id: in url
  349. Output:
  350. 200
  351. """
  352. user = g.user
  353. contact = Contact.get(contact_id)
  354. if not contact or contact.alias.user_id != user.id:
  355. return jsonify(error="Forbidden"), 403
  356. Contact.delete(contact_id)
  357. db.session.commit()
  358. return jsonify(deleted=True), 200