diff --git a/.github/helper/semgrep_rules/translate.yml b/.github/helper/semgrep_rules/translate.yml index df55089b9f..7754b52efc 100644 --- a/.github/helper/semgrep_rules/translate.yml +++ b/.github/helper/semgrep_rules/translate.yml @@ -42,7 +42,7 @@ rules: - id: frappe-translation-python-splitting pattern-either: - - pattern: _(...) + ... + _(...) + - pattern: _(...) + _(...) - pattern: _("..." + "...") - pattern-regex: '_\([^\)]*\\\s*' # lines broken by `\` - pattern-regex: '_\(\s*\n' # line breaks allowed by python for using ( ) diff --git a/frappe/__init__.py b/frappe/__init__.py index 5680ba86b5..2436692c81 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -10,11 +10,10 @@ be used to build database driven apps. Read the documentation: https://frappeframework.com/docs """ -from __future__ import unicode_literals, print_function -from six import iteritems, binary_type, text_type, string_types, PY2 +from six import iteritems, binary_type, text_type, string_types from werkzeug.local import Local, release_local -import os, sys, importlib, inspect, json +import os, sys, importlib, inspect, json, warnings import typing from past.builtins import cmp import click @@ -27,19 +26,14 @@ from .utils.lazy_loader import lazy_import # Lazy imports faker = lazy_import('faker') - -# Harmless for Python 3 -# For Python 2 set default encoding to utf-8 -if PY2: - reload(sys) - sys.setdefaultencoding("utf-8") - __version__ = '14.0.0-dev' __title__ = "Frappe Framework" local = Local() controllers = {} +warnings.simplefilter('always', DeprecationWarning) +warnings.simplefilter('always', PendingDeprecationWarning) class _dict(dict): """dict like object that exposes keys as attributes""" diff --git a/frappe/app.py b/frappe/app.py index ac588c0945..a72f343532 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -185,7 +185,7 @@ def make_form_dict(request): args = request.form or request.args if not isinstance(args, dict): - frappe.throw("Invalid request arguments") + frappe.throw(_("Invalid request arguments")) try: frappe.local.form_dict = frappe._dict({ k:v[0] if isinstance(v, (list, tuple)) else v \ @@ -296,7 +296,7 @@ def serve(port=8000, profile=False, no_reload=False, no_threading=False, site=No from werkzeug.serving import run_simple patch_werkzeug_reloader() - if profile: + if profile or os.environ.get('USE_PROFILER'): application = ProfilerMiddleware(application, sort_by=('cumtime', 'calls')) if not os.environ.get('NO_STATICS'): diff --git a/frappe/commands/site.py b/frappe/commands/site.py index ebd2700c9c..22a063651c 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -254,9 +254,7 @@ def list_apps(context, format): frappe.destroy() if format == "json": - import json - - click.echo(json.dumps(summary_dict)) + click.echo(frappe.as_json(summary_dict)) @click.command('add-system-manager') @click.argument('email') diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index c4b6cf4655..b917126696 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -96,22 +96,54 @@ def destroy_all_sessions(context, reason=None): raise SiteNotSpecifiedError @click.command('show-config') +@click.option("--format", "-f", type=click.Choice(["text", "json"]), default="text") @pass_context -def show_config(context): - "print configuration file" - print("\t\033[92m{:<50}\033[0m \033[92m{:<15}\033[0m".format('Config','Value')) - sites_path = os.path.join(frappe.utils.get_bench_path(), 'sites') - site_path = context.sites[0] - configuration = frappe.get_site_config(sites_path=sites_path, site_path=site_path) - print_config(configuration) +def show_config(context, format): + "Print configuration file to STDOUT in speified format" + if not context.sites: + raise SiteNotSpecifiedError -def print_config(config): - for conf, value in config.items(): - if isinstance(value, dict): - print_config(value) - else: - print("\t{:<50} {:<15}".format(conf, value)) + sites_config = {} + sites_path = os.getcwd() + + from frappe.utils.commands import render_table + + def transform_config(config, prefix=None): + prefix = f"{prefix}." if prefix else "" + site_config = [] + + for conf, value in config.items(): + if isinstance(value, dict): + site_config += transform_config(value, prefix=f"{prefix}{conf}") + else: + log_value = json.dumps(value) if isinstance(value, list) else value + site_config += [[f"{prefix}{conf}", log_value]] + + return site_config + + for site in context.sites: + frappe.init(site) + + if len(context.sites) != 1 and format == "text": + if context.sites.index(site) != 0: + click.echo() + click.secho(f"Site {site}", fg="yellow") + + configuration = frappe.get_site_config(sites_path=sites_path, site_path=site) + + if format == "text": + data = transform_config(configuration) + data.insert(0, ['Config','Value']) + render_table(data) + + if format == "json": + sites_config[site] = configuration + + frappe.destroy() + + if format == "json": + click.echo(frappe.as_json(sites_config)) @click.command('reset-perms') @@ -470,6 +502,7 @@ def console(context): locals()[app] = __import__(app) except ModuleNotFoundError: failed_to_import.append(app) + all_apps.remove(app) print("Apps in this namespace:\n{}".format(", ".join(all_apps))) if failed_to_import: @@ -657,20 +690,27 @@ def make_app(destination, app_name): @click.command('set-config') @click.argument('key') @click.argument('value') -@click.option('-g', '--global', 'global_', is_flag = True, default = False, help = 'Set Global Site Config') -@click.option('--as-dict', is_flag=True, default=False) +@click.option('-g', '--global', 'global_', is_flag=True, default=False, help='Set value in bench config') +@click.option('-p', '--parse', is_flag=True, default=False, help='Evaluate as Python Object') +@click.option('--as-dict', is_flag=True, default=False, help='Legacy: Evaluate as Python Object') @pass_context -def set_config(context, key, value, global_ = False, as_dict=False): +def set_config(context, key, value, global_=False, parse=False, as_dict=False): "Insert/Update a value in site_config.json" from frappe.installer import update_site_config - import ast + if as_dict: + from frappe.utils.commands import warn + warn("--as-dict will be deprecated in v14. Use --parse instead", category=PendingDeprecationWarning) + parse = as_dict + + if parse: + import ast value = ast.literal_eval(value) if global_: - sites_path = os.getcwd() # big assumption. + sites_path = os.getcwd() common_site_config_path = os.path.join(sites_path, 'common_site_config.json') - update_site_config(key, value, validate = False, site_config_path = common_site_config_path) + update_site_config(key, value, validate=False, site_config_path=common_site_config_path) else: for site in context.sites: frappe.init(site=site) @@ -727,50 +767,6 @@ def rebuild_global_search(context, static_pages=False): if not context.sites: raise SiteNotSpecifiedError -@click.command('auto-deploy') -@click.argument('app') -@click.option('--migrate', is_flag=True, default=False, help='Migrate after pulling') -@click.option('--restart', is_flag=True, default=False, help='Restart after migration') -@click.option('--remote', default='upstream', help='Remote, default is "upstream"') -@pass_context -def auto_deploy(context, app, migrate=False, restart=False, remote='upstream'): - '''Pull and migrate sites that have new version''' - from frappe.utils.gitutils import get_app_branch - from frappe.utils import get_sites - - branch = get_app_branch(app) - app_path = frappe.get_app_path(app) - - # fetch - subprocess.check_output(['git', 'fetch', remote, branch], cwd = app_path) - - # get diff - if subprocess.check_output(['git', 'diff', '{0}..{1}/{0}'.format(branch, remote)], cwd = app_path): - print('Updates found for {0}'.format(app)) - if app=='frappe': - # run bench update - import shlex - subprocess.check_output(shlex.split('bench update --no-backup'), cwd = '..') - else: - updated = False - subprocess.check_output(['git', 'pull', '--rebase', remote, branch], - cwd = app_path) - # find all sites with that app - for site in get_sites(): - frappe.init(site) - if app in frappe.get_installed_apps(): - print('Updating {0}'.format(site)) - updated = True - subprocess.check_output(['bench', '--site', site, 'clear-cache'], cwd = '..') - if migrate: - subprocess.check_output(['bench', '--site', site, 'migrate'], cwd = '..') - frappe.destroy() - - if updated or restart: - subprocess.check_output(['bench', 'restart'], cwd = '..') - else: - print('No Updates') - commands = [ build, diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index 731cb85d7c..d3017055cf 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -272,22 +272,13 @@ def prepare_to_notify(doc, print_html=None, print_format=None, attachments=None) doc.attachments.append(a) def set_incoming_outgoing_accounts(doc): - doc.incoming_email_account = doc.outgoing_email_account = None + from frappe.email.doctype.email_account.email_account import EmailAccount + incoming_email_account = EmailAccount.find_incoming( + match_by_email=doc.sender, match_by_doctype=doc.reference_doctype) + doc.incoming_email_account = incoming_email_account.email_id if incoming_email_account else None - if not doc.incoming_email_account and doc.sender: - doc.incoming_email_account = frappe.db.get_value("Email Account", - {"email_id": doc.sender, "enable_incoming": 1}, "email_id") - - if not doc.incoming_email_account and doc.reference_doctype: - doc.incoming_email_account = frappe.db.get_value("Email Account", - {"append_to": doc.reference_doctype, }, "email_id") - - if not doc.incoming_email_account: - doc.incoming_email_account = frappe.db.get_value("Email Account", - {"default_incoming": 1, "enable_incoming": 1}, "email_id") - - doc.outgoing_email_account = frappe.email.smtp.get_outgoing_email_account(raise_exception_not_set=False, - append_to=doc.doctype, sender=doc.sender) + doc.outgoing_email_account = EmailAccount.find_outgoing( + match_by_email=doc.sender, match_by_doctype=doc.reference_doctype) if doc.sent_or_received == "Sent": doc.db_set("email_account", doc.outgoing_email_account.name) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 9589507ca6..befaf7b01f 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -377,10 +377,17 @@ def handle_duration_fieldtype_values(result, columns): if fieldtype == "Duration": for entry in range(0, len(result)): - val_in_seconds = result[entry][i] - if val_in_seconds: - duration_val = format_duration(val_in_seconds) - result[entry][i] = duration_val + row = result[entry] + if isinstance(row, dict): + val_in_seconds = row[col.fieldname] + if val_in_seconds: + duration_val = format_duration(val_in_seconds) + row[col.fieldname] = duration_val + else: + val_in_seconds = row[i] + if val_in_seconds: + duration_val = format_duration(val_in_seconds) + row[i] = duration_val return result diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index 4869c5a9bf..3aa7c10ea5 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -8,9 +8,14 @@ import re import json import socket import time -from frappe import _ +import functools + +import email.utils + +from frappe import _, are_emails_muted from frappe.model.document import Document -from frappe.utils import validate_email_address, cint, cstr, get_datetime, DATE_FORMAT, strip, comma_or, sanitize_html, add_days +from frappe.utils import (validate_email_address, cint, cstr, get_datetime, + DATE_FORMAT, strip, comma_or, sanitize_html, add_days, parse_addr) from frappe.utils.user import is_system_user from frappe.utils.jinja import render_template from frappe.email.smtp import SMTPServer @@ -21,17 +26,40 @@ from datetime import datetime, timedelta from frappe.desk.form import assign_to from frappe.utils.user import get_system_managers from frappe.utils.background_jobs import enqueue, get_jobs -from frappe.core.doctype.communication.email import set_incoming_outgoing_accounts from frappe.utils.html_utils import clean_email_html +from frappe.utils.error import raise_error_on_no_output from frappe.email.utils import get_port +OUTGOING_EMAIL_ACCOUNT_MISSING = _("Please setup default Email Account from Setup > Email > Email Account") + class SentEmailInInbox(Exception): pass class InvalidEmailCredentials(frappe.ValidationError): pass +def cache_email_account(cache_name): + def decorator_cache_email_account(func): + @functools.wraps(func) + def wrapper_cache_email_account(*args, **kwargs): + if not hasattr(frappe.local, cache_name): + setattr(frappe.local, cache_name, {}) + + cached_accounts = getattr(frappe.local, cache_name) + match_by = list(kwargs.values()) + ['default'] + matched_accounts = list(filter(None, [cached_accounts.get(key) for key in match_by])) + if matched_accounts: + return matched_accounts[0] + + matched_accounts = func(*args, **kwargs) + cached_accounts.update(matched_accounts or {}) + return matched_accounts and list(matched_accounts.values())[0] + return wrapper_cache_email_account + return decorator_cache_email_account + class EmailAccount(Document): + DOCTYPE = 'Email Account' + def autoname(self): """Set name as `email_account_name` or make title from Email Address.""" if not self.email_account_name: @@ -249,6 +277,15 @@ class EmailAccount(Document): else: raise + @property + def _password(self): + raise_exception = not self.no_smtp_authentication + return self.get_password(raise_exception=raise_exception) + + @property + def default_sender(self): + return email.utils.formataddr((self.name, self.get("email_id"))) + @classmethod def throw_invalid_credentials_exception(cls): frappe.throw( @@ -257,6 +294,114 @@ class EmailAccount(Document): title=_("Invalid Credentials") ) + @classmethod + def from_record(cls, record): + email_account = frappe.new_doc(cls.DOCTYPE) + email_account.update(record) + return email_account + + @classmethod + def find(cls, name): + return frappe.get_doc(cls.DOCTYPE, name) + + @classmethod + def find_one_by_filters(cls, **kwargs): + name = frappe.db.get_value(cls.DOCTYPE, kwargs) + return cls.find(name) if name else None + + @classmethod + def find_from_config(cls): + config = cls.get_account_details_from_site_config() + return cls.from_record(config) if config else None + + @classmethod + def create_dummy(cls): + return cls.from_record({"sender": "notifications@example.com"}) + + @classmethod + @raise_error_on_no_output( + keep_quiet = lambda: not cint(frappe.get_system_settings('setup_complete')), + error_message = OUTGOING_EMAIL_ACCOUNT_MISSING, error_type = frappe.OutgoingEmailError) # noqa + @cache_email_account('outgoing_email_account') + def find_outgoing(cls, match_by_email=None, match_by_doctype=None, _raise_error=False): + """Find the outgoing Email account to use. + + :param match_by_email: Find account using emailID + :param match_by_doctype: Find account by matching `Append To` doctype + :param _raise_error: This is used by raise_error_on_no_output decorator to raise error. + """ + if match_by_email: + match_by_email = parse_addr(match_by_email)[1] + doc = cls.find_one_by_filters(enable_outgoing=1, email_id=match_by_email) + if doc: + return {match_by_email: doc} + + if match_by_doctype: + doc = cls.find_one_by_filters(enable_outgoing=1, enable_incoming=1, append_to=match_by_doctype) + if doc: + return {match_by_doctype: doc} + + doc = cls.find_default_outgoing() + if doc: + return {'default': doc} + + @classmethod + def find_default_outgoing(cls): + """ Find default outgoing account. + """ + doc = cls.find_one_by_filters(enable_outgoing=1, default_outgoing=1) + doc = doc or cls.find_from_config() + return doc or (are_emails_muted() and cls.create_dummy()) + + @classmethod + def find_incoming(cls, match_by_email=None, match_by_doctype=None): + """Find the incoming Email account to use. + :param match_by_email: Find account using emailID + :param match_by_doctype: Find account by matching `Append To` doctype + """ + doc = cls.find_one_by_filters(enable_incoming=1, email_id=match_by_email) + if doc: + return doc + + doc = cls.find_one_by_filters(enable_incoming=1, append_to=match_by_doctype) + if doc: + return doc + + doc = cls.find_default_incoming() + return doc + + @classmethod + def find_default_incoming(cls): + doc = cls.find_one_by_filters(enable_incoming=1, default_incoming=1) + return doc + + @classmethod + def get_account_details_from_site_config(cls): + if not frappe.conf.get("mail_server"): + return {} + + field_to_conf_name_map = { + 'smtp_server': {'conf_names': ('mail_server',)}, + 'smtp_port': {'conf_names': ('mail_port',)}, + 'use_tls': {'conf_names': ('use_tls', 'mail_login')}, + 'login_id': {'conf_names': ('mail_login',)}, + 'email_id': {'conf_names': ('auto_email_id', 'mail_login'), 'default': 'notifications@example.com'}, + 'password': {'conf_names': ('mail_password',)}, + 'always_use_account_email_id_as_sender': + {'conf_names': ('always_use_account_email_id_as_sender',), 'default': 0}, + 'always_use_account_name_as_sender_name': + {'conf_names': ('always_use_account_name_as_sender_name',), 'default': 0}, + 'name': {'conf_names': ('email_sender_name',), 'default': 'Frappe'}, + 'from_site_config': {'default': True} + } + + account_details = {} + for doc_field_name, d in field_to_conf_name_map.items(): + conf_names, default = d.get('conf_names') or [], d.get('default') + value = [frappe.conf.get(k) for k in conf_names if frappe.conf.get(k)] + account_details[doc_field_name] = (value and value[0]) or default + return account_details + def handle_incoming_connect_error(self, description): if test_internet(): if self.get_failed_attempts_count() > 2: @@ -642,6 +787,8 @@ class EmailAccount(Document): def send_auto_reply(self, communication, email): """Send auto reply if set.""" + from frappe.core.doctype.communication.email import set_incoming_outgoing_accounts + if self.enable_auto_reply: set_incoming_outgoing_accounts(communication) @@ -653,7 +800,7 @@ class EmailAccount(Document): frappe.sendmail(recipients = [email.from_email], sender = self.email_id, reply_to = communication.incoming_email_account, - subject = _("Re: ") + communication.subject, + subject = " ".join([_("Re:"), communication.subject]), content = render_template(self.auto_reply_message or "", communication.as_dict()) or \ frappe.get_template("templates/emails/auto_reply.html").render(communication.as_dict()), reference_doctype = communication.reference_doctype, diff --git a/frappe/email/doctype/email_domain/test_records.json b/frappe/email/doctype/email_domain/test_records.json index 32bc66e150..a6ccc99f06 100644 --- a/frappe/email/doctype/email_domain/test_records.json +++ b/frappe/email/doctype/email_domain/test_records.json @@ -10,7 +10,8 @@ "incoming_port": "993", "attachment_limit": "1", "smtp_server": "smtp.test.com", - "smtp_port": "587" + "smtp_port": "587", + "password": "password" }, { "doctype": "Email Account", @@ -25,6 +26,7 @@ "incoming_port": "143", "attachment_limit": "1", "smtp_server": "smtp.test.com", - "smtp_port": "587" + "smtp_port": "587", + "password": "password" } ] diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py index 3dcdf00a8e..45888119ea 100755 --- a/frappe/email/email_body.py +++ b/frappe/email/email_body.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe, re, os from frappe.utils.pdf import get_pdf -from frappe.email.smtp import get_outgoing_email_account +from frappe.email.doctype.email_account.email_account import EmailAccount from frappe.utils import (get_url, scrub_urls, strip, expand_relative_urls, cint, split_emails, to_markdown, markdown, random_string, parse_addr) import email.utils @@ -75,7 +75,8 @@ class EMail: self.bcc = bcc or [] self.html_set = False - self.email_account = email_account or get_outgoing_email_account(sender=sender) + self.email_account = email_account or \ + EmailAccount.find_outgoing(match_by_email=sender, _raise_error=True) def set_html(self, message, text_content = None, footer=None, print_html=None, formatted=None, inline_images=None, header=None): @@ -249,8 +250,8 @@ class EMail: def get_formatted_html(subject, message, footer=None, print_html=None, email_account=None, header=None, unsubscribe_link=None, sender=None, with_container=False): - if not email_account: - email_account = get_outgoing_email_account(False, sender=sender) + + email_account = email_account or EmailAccount.find_outgoing(match_by_email=sender) signature = None if "" not in message: @@ -480,4 +481,4 @@ def sanitize_email_header(str): return str.replace('\r', '').replace('\n', '') def get_brand_logo(email_account): - return email_account.get('brand_logo') \ No newline at end of file + return email_account.get('brand_logo') diff --git a/frappe/email/queue.py b/frappe/email/queue.py index 2aff04edc9..cd984e9bf9 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -7,7 +7,8 @@ import sys from six.moves import html_parser as HTMLParser import smtplib, quopri, json from frappe import msgprint, _, safe_decode, safe_encode, enqueue -from frappe.email.smtp import SMTPServer, get_outgoing_email_account +from frappe.email.smtp import SMTPServer +from frappe.email.doctype.email_account.email_account import EmailAccount from frappe.email.email_body import get_email, get_formatted_html, add_attachment from frappe.utils.verified_command import get_signed_params, verify_request from html2text import html2text @@ -73,7 +74,9 @@ def send(recipients=None, sender=None, subject=None, message=None, text_content= if isinstance(send_after, int): send_after = add_days(nowdate(), send_after) - email_account = get_outgoing_email_account(True, append_to=reference_doctype, sender=sender) + email_account = EmailAccount.find_outgoing( + match_by_doctype=reference_doctype, match_by_email=sender, _raise_error=True) + if not sender or sender == "Administrator": sender = email_account.default_sender @@ -516,7 +519,7 @@ def prepare_message(email, recipient, recipients_list): return "" # Parse "Email Account" from "Email Sender" - email_account = get_outgoing_email_account(raise_exception_not_set=False, sender=email.sender) + email_account = EmailAccount.find_outgoing(match_by_email=email.sender) if frappe.conf.use_ssl and email_account.track_email_status: # Using SSL => Publically available domain => Email Read Reciept Possible message = message.replace("", quopri.encodestring(''.format(frappe.local.site, email.communication).encode()).decode()) diff --git a/frappe/email/smtp.py b/frappe/email/smtp.py index 9ba81fa146..ca69e621cc 100644 --- a/frappe/email/smtp.py +++ b/frappe/email/smtp.py @@ -34,126 +34,6 @@ def send(email, append_to=None, retry=1): _send(retry) -def get_outgoing_email_account(raise_exception_not_set=True, append_to=None, sender=None): - """Returns outgoing email account based on `append_to` or the default - outgoing account. If default outgoing account is not found, it will - try getting settings from `site_config.json`.""" - - sender_email_id = None - _email_account = None - - if sender: - sender_email_id = parse_addr(sender)[1] - - if not getattr(frappe.local, "outgoing_email_account", None): - frappe.local.outgoing_email_account = {} - - if not (frappe.local.outgoing_email_account.get(append_to) - or frappe.local.outgoing_email_account.get(sender_email_id) - or frappe.local.outgoing_email_account.get("default")): - email_account = None - - if sender_email_id: - # check if the sender has an email account with enable_outgoing - email_account = _get_email_account({"enable_outgoing": 1, - "email_id": sender_email_id}) - - if not email_account and append_to: - # append_to is only valid when enable_incoming is checked - email_accounts = frappe.db.get_values("Email Account", { - "enable_outgoing": 1, - "enable_incoming": 1, - "append_to": append_to, - }, cache=True) - - if email_accounts: - _email_account = email_accounts[0] - - else: - email_account = _get_email_account({ - "enable_outgoing": 1, - "enable_incoming": 1, - "append_to": append_to - }) - - if not email_account: - # sender don't have the outging email account - sender_email_id = None - email_account = get_default_outgoing_email_account(raise_exception_not_set=raise_exception_not_set) - - if not email_account and _email_account: - # if default email account is not configured then setup first email account based on append to - email_account = _email_account - - if not email_account and raise_exception_not_set and cint(frappe.db.get_single_value('System Settings', 'setup_complete')): - frappe.throw(_("Please setup default Email Account from Setup > Email > Email Account"), - frappe.OutgoingEmailError) - - if email_account: - if email_account.enable_outgoing and not getattr(email_account, 'from_site_config', False): - raise_exception = True - if email_account.smtp_server in ['localhost','127.0.0.1'] or email_account.no_smtp_authentication: - raise_exception = False - email_account.password = email_account.get_password(raise_exception=raise_exception) - email_account.default_sender = email.utils.formataddr((email_account.name, email_account.get("email_id"))) - - frappe.local.outgoing_email_account[append_to or sender_email_id or "default"] = email_account - - return frappe.local.outgoing_email_account.get(append_to) \ - or frappe.local.outgoing_email_account.get(sender_email_id) \ - or frappe.local.outgoing_email_account.get("default") - -def get_default_outgoing_email_account(raise_exception_not_set=True): - '''conf should be like: - { - "mail_server": "smtp.example.com", - "mail_port": 587, - "use_tls": 1, - "mail_login": "emails@example.com", - "mail_password": "Super.Secret.Password", - "auto_email_id": "emails@example.com", - "email_sender_name": "Example Notifications", - "always_use_account_email_id_as_sender": 0, - "always_use_account_name_as_sender_name": 0 - } - ''' - email_account = _get_email_account({"enable_outgoing": 1, "default_outgoing": 1}) - if email_account: - email_account.password = email_account.get_password(raise_exception=False) - - if not email_account and frappe.conf.get("mail_server"): - # from site_config.json - email_account = frappe.new_doc("Email Account") - email_account.update({ - "smtp_server": frappe.conf.get("mail_server"), - "smtp_port": frappe.conf.get("mail_port"), - - # legacy: use_ssl was used in site_config instead of use_tls, but meant the same thing - "use_tls": cint(frappe.conf.get("use_tls") or 0) or cint(frappe.conf.get("use_ssl") or 0), - "login_id": frappe.conf.get("mail_login"), - "email_id": frappe.conf.get("auto_email_id") or frappe.conf.get("mail_login") or 'notifications@example.com', - "password": frappe.conf.get("mail_password"), - "always_use_account_email_id_as_sender": frappe.conf.get("always_use_account_email_id_as_sender", 0), - "always_use_account_name_as_sender_name": frappe.conf.get("always_use_account_name_as_sender_name", 0) - }) - email_account.from_site_config = True - email_account.name = frappe.conf.get("email_sender_name") or "Frappe" - - if not email_account and not raise_exception_not_set: - return None - - if frappe.are_emails_muted(): - # create a stub - email_account = frappe.new_doc("Email Account") - email_account.update({ - "email_id": "notifications@example.com" - }) - - return email_account - -def _get_email_account(filters): - name = frappe.db.get_value("Email Account", filters) - return frappe.get_doc("Email Account", name) if name else None class SMTPServer: def __init__(self, login=None, password=None, server=None, port=None, use_tls=None, use_ssl=None, append_to=None): @@ -176,17 +56,15 @@ class SMTPServer: self.setup_email_account(append_to) def setup_email_account(self, append_to=None, sender=None): - self.email_account = get_outgoing_email_account(raise_exception_not_set=False, append_to=append_to, sender=sender) + from frappe.email.doctype.email_account.email_account import EmailAccount + self.email_account = EmailAccount.find_outgoing(match_by_doctype=append_to, match_by_email=sender) if self.email_account: self.server = self.email_account.smtp_server self.login = (getattr(self.email_account, "login_id", None) or self.email_account.email_id) - if not self.email_account.no_smtp_authentication: - if self.email_account.ascii_encode_password: - self.password = frappe.safe_encode(self.email_account.password, 'ascii') - else: - self.password = self.email_account.password - else: + if self.email_account.no_smtp_authentication or frappe.local.flags.in_test: self.password = None + else: + self.password = self.email_account._password self.port = self.email_account.smtp_port self.use_tls = self.email_account.use_tls self.sender = self.email_account.email_id diff --git a/frappe/email/test_smtp.py b/frappe/email/test_smtp.py index 0b11c559a2..e170617383 100644 --- a/frappe/email/test_smtp.py +++ b/frappe/email/test_smtp.py @@ -4,7 +4,7 @@ import unittest import frappe from frappe.email.smtp import SMTPServer -from frappe.email.smtp import get_outgoing_email_account +from frappe.email.doctype.email_account.email_account import EmailAccount class TestSMTP(unittest.TestCase): def test_smtp_ssl_session(self): @@ -33,13 +33,13 @@ class TestSMTP(unittest.TestCase): frappe.local.outgoing_email_account = {} # lowest preference given to email account with default incoming enabled - create_email_account(email_id="default_outgoing_enabled@gmail.com", password="***", enable_outgoing = 1, default_outgoing=1) - self.assertEqual(get_outgoing_email_account().email_id, "default_outgoing_enabled@gmail.com") + create_email_account(email_id="default_outgoing_enabled@gmail.com", password="password", enable_outgoing = 1, default_outgoing=1) + self.assertEqual(EmailAccount.find_outgoing().email_id, "default_outgoing_enabled@gmail.com") frappe.local.outgoing_email_account = {} # highest preference given to email account with append_to matching - create_email_account(email_id="append_to@gmail.com", password="***", enable_outgoing = 1, default_outgoing=1, append_to="Blog Post") - self.assertEqual(get_outgoing_email_account(append_to="Blog Post").email_id, "append_to@gmail.com") + create_email_account(email_id="append_to@gmail.com", password="password", enable_outgoing = 1, default_outgoing=1, append_to="Blog Post") + self.assertEqual(EmailAccount.find_outgoing(match_by_doctype="Blog Post").email_id, "append_to@gmail.com") # add back the mail_server frappe.conf['mail_server'] = mail_server diff --git a/frappe/public/js/frappe/dom.js b/frappe/public/js/frappe/dom.js index db9407ed53..2769e9061d 100644 --- a/frappe/public/js/frappe/dom.js +++ b/frappe/public/js/frappe/dom.js @@ -319,7 +319,7 @@ frappe.get_data_pill = (label, target_id=null, remove_action=null, image=null) = frappe.get_modal = function(title, content) { return $(`