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 $(`