def get_or_create_contact(contact_from_header: str, mail_from: str, alias: Alias) -> Contact: """ contact_from_header is the RFC 2047 format FROM header """ # contact_from_header can be None, use mail_from in this case instead contact_from_header = contact_from_header or mail_from # force convert header to string, sometimes contact_from_header is Header object contact_from_header = str(contact_from_header) contact_name, contact_email = parseaddr_unicode(contact_from_header) if not contact_email: # From header is wrongly formatted, try with mail_from LOG.warning("From header is empty, parse mail_from %s %s", mail_from, alias) contact_name, contact_email = parseaddr_unicode(mail_from) if not contact_email: LOG.exception( "Cannot parse contact from from_header:%s, mail_from:%s", contact_from_header, mail_from, ) contact = Contact.get_by(alias_id=alias.id, website_email=contact_email) if contact: if contact.name != contact_name: LOG.d( "Update contact %s name %s to %s", contact, contact.name, contact_name, ) contact.name = contact_name db.session.commit() else: LOG.debug( "create contact for alias %s and contact %s", alias, contact_from_header, ) reply_email = generate_reply_email() try: contact = Contact.create( user_id=alias.user_id, alias_id=alias.id, website_email=contact_email, name=contact_name, reply_email=reply_email, ) db.session.commit() except IntegrityError: LOG.warning("Contact %s %s already exist", alias, contact_email) db.session.rollback() contact = Contact.get_by(alias_id=alias.id, website_email=contact_email) return contact
def test_parseaddr_unicode(): # only email assert parseaddr_unicode("*****@*****.**") == ( "", "*****@*****.**", ) # ascii address assert parseaddr_unicode("First Last <*****@*****.**>") == ( "First Last", "*****@*****.**", ) # Handle quote assert parseaddr_unicode('"First Last" <*****@*****.**>') == ( "First Last", "*****@*****.**", ) # UTF-8 charset assert parseaddr_unicode( "=?UTF-8?B?TmjGoW4gTmd1eeG7hW4=?= <*****@*****.**>") == ( "Nhơn Nguyễn", "*****@*****.**", ) # iso-8859-1 charset assert parseaddr_unicode("=?iso-8859-1?q?p=F6stal?= <*****@*****.**>") == ( "pöstal", "*****@*****.**", )
def website_send_to(self): """return the email address with name. to use when user wants to send an email from the alias Return "First Last | email at example.com" <ra+random_string@SL> """ # Prefer using contact name if possible name = self.name # if no name, try to parse it from website_from if not name and self.website_from: try: from app.email_utils import parseaddr_unicode name, _ = parseaddr_unicode(self.website_from) except Exception: # Skip if website_from is wrongly formatted LOG.warning( "Cannot parse contact %s website_from %s", self, self.website_from ) name = "" # remove all double quote if name: name = name.replace('"', "") if name: name = name + " | " + self.website_email.replace("@", " at ") else: name = self.website_email.replace("@", " at ") # cannot use formataddr here as this field is for email client, not for MTA return f'"{name}" <{self.reply_email}>'
def test_new_addr(flask_client): user = User.create( email="[email protected]", password="******", name="Test User", activated=True, commit=True, sender_format=1, ) alias = Alias.create_new_random(user) db.session.commit() # default sender_format is 'via' c1 = Contact.create( user_id=user.id, alias_id=alias.id, website_email="*****@*****.**", reply_email="rep@SL", name="First Last", commit=True, ) assert c1.new_addr() == '"[email protected] via SimpleLogin" <rep@SL>' # set sender format = FULL user.sender_format = SenderFormatEnum.FULL.value db.session.commit() assert c1.new_addr() == '"First Last - [email protected]" <rep@SL>' # Make sure email isn't duplicated if sender name equals email c1.name = "*****@*****.**" db.session.commit() assert c1.new_addr() == '"*****@*****.**" <rep@SL>' # set sender_format = AT user.sender_format = SenderFormatEnum.AT.value c1.name = "First Last" db.session.commit() assert c1.new_addr() == '"First Last - abcd at example.com" <rep@SL>' # unicode name c1.name = "Nhơn Nguyễn" db.session.commit() assert ( c1.new_addr() == "=?utf-8?q?Nh=C6=A1n_Nguy=E1=BB=85n_-_abcd_at_example=2Ecom?= <rep@SL>" ) # sanity check for parseaddr_unicode assert parseaddr_unicode(c1.new_addr()) == ( "Nhơn Nguyễn - abcd at example.com", "rep@sl", )
def create_contact_route(alias_id): """ Create contact for an alias Input: alias_id: in url contact: in body Output: 201 if success 409 if contact already added """ data = request.get_json() if not data: return jsonify(error="request body cannot be empty"), 400 user = g.user alias: Alias = Alias.get(alias_id) if alias.user_id != user.id: return jsonify(error="Forbidden"), 403 contact_addr = data.get("contact") if not contact_addr: return jsonify(error="Contact cannot be empty"), 400 contact_name, contact_email = parseaddr_unicode(contact_addr) if not is_valid_email(contact_email): return jsonify(error=f"invalid contact email {contact_email}"), 400 contact_email = sanitize_email(contact_email) # already been added contact = Contact.get_by(alias_id=alias.id, website_email=contact_email) if contact: return jsonify(**serialize_contact(contact, existed=True)), 200 contact = Contact.create( user_id=alias.user_id, alias_id=alias.id, website_email=contact_email, name=contact_name, reply_email=generate_reply_email(contact_email, user), ) LOG.d("create reverse-alias for %s %s", contact_addr, alias) db.session.commit() return jsonify(**serialize_contact(contact)), 201
def create_contact_route(alias_id): """ Create contact for an alias Input: alias_id: in url contact: in body Output: 201 if success 409 if contact already added """ data = request.get_json() if not data: return jsonify(error="request body cannot be empty"), 400 user = g.user alias: Alias = Alias.get(alias_id) if alias.user_id != user.id: return jsonify(error="Forbidden"), 403 contact_addr = data.get("contact") # generate a reply_email, make sure it is unique # not use while to avoid infinite loop reply_email = f"ra+{random_string(25)}@{EMAIL_DOMAIN}" for _ in range(1000): reply_email = f"ra+{random_string(25)}@{EMAIL_DOMAIN}" if not Contact.get_by(reply_email=reply_email): break contact_name, contact_email = parseaddr_unicode(contact_addr) # already been added if Contact.get_by(alias_id=alias.id, website_email=contact_email): return jsonify(error="Contact already added"), 409 contact = Contact.create( user_id=alias.user_id, alias_id=alias.id, website_email=contact_email, name=contact_name, reply_email=reply_email, ) LOG.d("create reverse-alias for %s %s", contact_addr, alias) db.session.commit() return jsonify(**serialize_contact(contact)), 201
def test_parseaddr_unicode(): # only email assert parseaddr_unicode("*****@*****.**") == ( "", "*****@*****.**", ) # ascii address assert parseaddr_unicode("First Last <*****@*****.**>") == ( "First Last", "*****@*****.**", ) # Handle quote assert parseaddr_unicode('"First Last" <*****@*****.**>') == ( "First Last", "*****@*****.**", ) # UTF-8 charset assert parseaddr_unicode( "=?UTF-8?B?TmjGoW4gTmd1eeG7hW4=?= <*****@*****.**>") == ( "Nhơn Nguyễn", "*****@*****.**", ) # iso-8859-1 charset assert parseaddr_unicode("=?iso-8859-1?q?p=F6stal?= <*****@*****.**>") == ( "pöstal", "*****@*****.**", ) # when a name can't be decoded, return an empty string assert parseaddr_unicode("=?UTF-8?B?Cec���?= <*****@*****.**>") == ( "", "*****@*****.**", )
def replace_header_when_forward(msg: Message, alias: Alias, header: str): """ Replace CC or To header by Reply emails in forward phase """ addrs = get_addrs_from_header(msg, header) # Nothing to do if not addrs: return new_addrs: [str] = [] need_replace = False for addr in addrs: contact_name, contact_email = parseaddr_unicode(addr) # no transformation when alias is already in the header if contact_email == alias.email: new_addrs.append(addr) continue contact = Contact.get_by(alias_id=alias.id, website_email=contact_email) if contact: # update the contact name if needed if contact.name != contact_name: LOG.d( "Update contact %s name %s to %s", contact, contact.name, contact_name, ) contact.name = contact_name db.session.commit() else: LOG.debug( "create contact for alias %s and email %s, header %s", alias, contact_email, header, ) reply_email = generate_reply_email() contact = Contact.create( user_id=alias.user_id, alias_id=alias.id, website_email=contact_email, name=contact_name, reply_email=reply_email, is_cc=header.lower() == "cc", ) db.session.commit() new_addrs.append(contact.new_addr()) need_replace = True if need_replace: new_header = ",".join(new_addrs) LOG.d("Replace %s header, old: %s, new: %s", header, msg[header], new_header) add_or_replace_header(msg, header, new_header) else: LOG.d("No need to replace %s header", header)
def alias_contact_manager(alias_id): highlight_contact_id = None if request.args.get("highlight_contact_id"): highlight_contact_id = int(request.args.get("highlight_contact_id")) alias = Alias.get(alias_id) page = 0 if request.args.get("page"): page = int(request.args.get("page")) query = request.args.get("query") or "" # sanity check if not alias: flash("You do not have access to this page", "warning") return redirect(url_for("dashboard.index")) if alias.user_id != current_user.id: flash("You do not have access to this page", "warning") return redirect(url_for("dashboard.index")) new_contact_form = NewContactForm() if request.method == "POST": if request.form.get("form-name") == "create": if new_contact_form.validate(): contact_addr = new_contact_form.email.data.strip() try: contact_name, contact_email = parseaddr_unicode( contact_addr) contact_email = sanitize_email(contact_email) except Exception: flash(f"{contact_addr} is invalid", "error") return redirect( url_for( "dashboard.alias_contact_manager", alias_id=alias_id, )) if not is_valid_email(contact_email): flash(f"{contact_email} is invalid", "error") return redirect( url_for( "dashboard.alias_contact_manager", alias_id=alias_id, )) contact = Contact.get_by(alias_id=alias.id, website_email=contact_email) # already been added if contact: flash(f"{contact_email} is already added", "error") return redirect( url_for( "dashboard.alias_contact_manager", alias_id=alias_id, highlight_contact_id=contact.id, )) contact = Contact.create( user_id=alias.user_id, alias_id=alias.id, website_email=contact_email, name=contact_name, reply_email=generate_reply_email(contact_email, current_user), ) LOG.d("create reverse-alias for %s", contact_addr) db.session.commit() flash(f"Reverse alias for {contact_addr} is created", "success") return redirect( url_for( "dashboard.alias_contact_manager", alias_id=alias_id, highlight_contact_id=contact.id, )) elif request.form.get("form-name") == "delete": contact_id = request.form.get("contact-id") contact = Contact.get(contact_id) if not contact: flash("Unknown error. Refresh the page", "warning") return redirect( url_for("dashboard.alias_contact_manager", alias_id=alias_id)) elif contact.alias_id != alias.id: flash("You cannot delete reverse-alias", "warning") return redirect( url_for("dashboard.alias_contact_manager", alias_id=alias_id)) delete_contact_email = contact.website_email Contact.delete(contact_id) db.session.commit() flash(f"Reverse-alias for {delete_contact_email} has been deleted", "success") return redirect( url_for("dashboard.alias_contact_manager", alias_id=alias_id)) elif request.form.get("form-name") == "search": query = request.form.get("query") return redirect( url_for( "dashboard.alias_contact_manager", alias_id=alias_id, query=query, highlight_contact_id=highlight_contact_id, )) contact_infos = get_contact_infos(alias, page, query=query) last_page = len(contact_infos) < PAGE_LIMIT # if highlighted contact isn't included, fetch it # make sure highlighted contact is at array start contact_ids = [contact_info.contact.id for contact_info in contact_infos] if highlight_contact_id and highlight_contact_id not in contact_ids: contact_infos = (get_contact_infos( alias, contact_id=highlight_contact_id, query=query) + contact_infos) return render_template( "dashboard/alias_contact_manager.html", contact_infos=contact_infos, alias=alias, new_contact_form=new_contact_form, highlight_contact_id=highlight_contact_id, page=page, last_page=last_page, query=query, )
def alias_contact_manager(alias_id): highlight_contact_id = None if request.args.get("highlight_contact_id"): highlight_contact_id = int(request.args.get("highlight_contact_id")) alias = Alias.get(alias_id) # sanity check if not alias: flash("You do not have access to this page", "warning") return redirect(url_for("dashboard.index")) if alias.user_id != current_user.id: flash("You do not have access to this page", "warning") return redirect(url_for("dashboard.index")) new_contact_form = NewContactForm() if request.method == "POST": if request.form.get("form-name") == "create": if new_contact_form.validate(): contact_addr = new_contact_form.email.data.strip() # generate a reply_email, make sure it is unique # not use while to avoid infinite loop reply_email = f"ra+{random_string(25)}@{EMAIL_DOMAIN}" for _ in range(1000): reply_email = f"ra+{random_string(25)}@{EMAIL_DOMAIN}" if not Contact.get_by(reply_email=reply_email): break try: contact_name, contact_email = parseaddr_unicode( contact_addr) except Exception: flash(f"{contact_addr} is invalid", "error") return redirect( url_for( "dashboard.alias_contact_manager", alias_id=alias_id, )) contact_email = contact_email.lower() contact = Contact.get_by(alias_id=alias.id, website_email=contact_email) # already been added if contact: flash(f"{contact_email} is already added", "error") return redirect( url_for( "dashboard.alias_contact_manager", alias_id=alias_id, highlight_contact_id=contact.id, )) contact = Contact.create( user_id=alias.user_id, alias_id=alias.id, website_email=contact_email, name=contact_name, reply_email=reply_email, ) LOG.d("create reverse-alias for %s", contact_addr) db.session.commit() flash(f"Reverse alias for {contact_addr} is created", "success") return redirect( url_for( "dashboard.alias_contact_manager", alias_id=alias_id, highlight_contact_id=contact.id, )) elif request.form.get("form-name") == "delete": contact_id = request.form.get("contact-id") contact = Contact.get(contact_id) if not contact: flash("Unknown error. Refresh the page", "warning") return redirect( url_for("dashboard.alias_contact_manager", alias_id=alias_id)) elif contact.alias_id != alias.id: flash("You cannot delete reverse-alias", "warning") return redirect( url_for("dashboard.alias_contact_manager", alias_id=alias_id)) delete_contact_email = contact.website_email Contact.delete(contact_id) db.session.commit() flash(f"Reverse-alias for {delete_contact_email} has been deleted", "success") return redirect( url_for("dashboard.alias_contact_manager", alias_id=alias_id)) # make sure highlighted contact is at array start contacts = alias.contacts if highlight_contact_id: contacts = sorted(contacts, key=lambda fe: fe.id == highlight_contact_id, reverse=True) return render_template( "dashboard/alias_contact_manager.html", contacts=contacts, alias=alias, new_contact_form=new_contact_form, highlight_contact_id=highlight_contact_id, )