Repository: frappe/frappe

Issue:
IMAP port settings are not updated from Email Domain to Email Account
## Description of the issue

When changing the IMAP port in an existing Email Domain, the Email Accounts using this Domain are not updated accordingly. This can lead to Frappe trying an IMAPS connection (which usually is 993) to the plain IMAP port 143, resulting in misleading error messages like `ssl.SSLError: [SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:852)`.

We could track down the root cause to the method `on_update` from the DocType "Email Domain": it simply misses the field `incoming_port` when copying data to all e-mail accounts that use this domain. This leads to the problem if the `incoming_port` is already set in the email account and gets updated/changed afterwards in the email domain.
## Context information (for bug reports)

```
frappe-bench$ bench --version
5.0.0

frappe-bench$ bench version
erpnext 12.11.2
frappe 12.9.1
```
## Steps to reproduce the issue

1. To reproduce this small bug you need to create a "Email Domain" in Frappe and save it with imap-port 143 and no SSL.
2. Create an e-mail account and link it with the domain from step 1 but without `Enable Incoming` and save.
3. Try to `Enable Incoming` and save
4. After "saving" the e-mail account go to the domain and change the imap-port from 143 to 993 and check SSL.
5. The `incoming_port` in Email-account is still 143.

### Observed result
In the database you can see that the `incoming_port` in the e-mail account is still 143 (real domain and mail addresses hidden):

```
select
    ea.email_id,
    ea.domain,
    ea.incoming_port,
    ed.incoming_port,
    ea.email_server,
    ed.email_server
from 
    `tabEmail Account` ea,
    `tabEmail Domain` ed
where ea.domain = ed.name
    and ed.name = "example.com";
```

#### Before updating the IMAP port in the domain
```
+------------------+-------------+---------------+---------------+--------------+--------------+
| email_id         | domain      | incoming_port | incoming_port | email_server | email_server |
+------------------+-------------+---------------+---------------+--------------+--------------+
| test@example.com | example.com | 143           | 143           | example.com  | example.com  |
+------------------+-------------+---------------+---------------+--------------+--------------+
1 row in set (0.000 sec)
```
#### After updating the IMAP port in the domain
```
+------------------+-------------+---------------+---------------+--------------+--------------+
| email_id         | domain      | incoming_port | incoming_port | email_server | email_server |
+------------------+-------------+---------------+---------------+--------------+--------------+
| test@example.com | example.com | 143           | 993           | example.com  | example.com  |
+------------------+-------------+---------------+---------------+--------------+--------------+
1 row in set (0.001 sec)
```
Now it will always trigger an SSL-handshake-error if the scheduler tries to get access.

### Expected result
When the mail domain gets updated all necessary fields related to e-mail account should be updated including the `incoming_port`.
### Stacktrace / full error message

```
Traceback (most recent call last):
  File "/home/erpnext/frappe-bench/apps/frappe/frappe/app.py", line 64, in application
    response = frappe.api.handle()
  File "/home/erpnext/frappe-bench/apps/frappe/frappe/api.py", line 59, in handle
    return frappe.handler.handle()
  File "/home/erpnext/frappe-bench/apps/frappe/frappe/handler.py", line 24, in handle
    data = execute_cmd(cmd)
  File "/home/erpnext/frappe-bench/apps/frappe/frappe/handler.py", line 63, in execute_cmd
    return frappe.call(method, **frappe.form_dict)
  File "/home/erpnext/frappe-bench/apps/frappe/frappe/__init__.py", line 1055, in call
    return fn(*args, **newargs)
  File "/home/erpnext/frappe-bench/apps/frappe/frappe/desk/form/save.py", line 21, in savedocs
    doc.save()
  File "/home/erpnext/frappe-bench/apps/frappe/frappe/model/document.py", line 273, in save
    return self._save(*args, **kwargs)
  File "/home/erpnext/frappe-bench/apps/frappe/frappe/model/document.py", line 309, in _save
    self.run_before_save_methods()
  File "/home/erpnext/frappe-bench/apps/frappe/frappe/model/document.py", line 896, in run_before_save_methods
    self.run_method("validate")
  File "/home/erpnext/frappe-bench/apps/frappe/frappe/model/document.py", line 797, in run_method
    out = Document.hook(fn)(self, *args, **kwargs)
  File "/home/erpnext/frappe-bench/apps/frappe/frappe/model/document.py", line 1073, in composer
    return composed(self, method, *args, **kwargs)
  File "/home/erpnext/frappe-bench/apps/frappe/frappe/model/document.py", line 1056, in runner
    add_to_return_value(self, fn(self, *args, **kwargs))
  File "/home/erpnext/frappe-bench/apps/frappe/frappe/model/document.py", line 791, in <lambda>
    fn = lambda self, *args, **kwargs: getattr(self, method)(*args, **kwargs)
  File "/home/erpnext/frappe-bench/apps/frappe/frappe/email/doctype/email_account/email_account.py", line 68, in validate
    self.get_incoming_server()
  File "/home/erpnext/frappe-bench/apps/frappe/frappe/email/doctype/email_account/email_account.py", line 168, in get_incoming_server
    email_server.connect()
  File "/home/erpnext/frappe-bench/apps/frappe/frappe/email/receive.py", line 43, in connect
    return self.connect_imap()
  File "/home/erpnext/frappe-bench/apps/frappe/frappe/email/receive.py", line 51, in connect_imap
    self.imap = Timed_IMAP4_SSL(self.settings.host, self.settings.incoming_port, timeout=frappe.conf.get("pop_timeout"))
  File "/home/erpnext/frappe-bench/apps/frappe/frappe/email/receive.py", line 564, in __init__
    self._super.__init__(self, *args, **kwargs)
  File "/usr/lib/python3.6/imaplib.py", line 1288, in __init__
    IMAP4.__init__(self, host, port)
  File "/usr/lib/python3.6/imaplib.py", line 198, in __init__
    self.open(host, port)
  File "/usr/lib/python3.6/imaplib.py", line 1301, in open
    IMAP4.open(self, host, port)
  File "/usr/lib/python3.6/imaplib.py", line 299, in open
    self.sock = self._create_socket()
  File "/usr/lib/python3.6/imaplib.py", line 1293, in _create_socket
    server_hostname=self.host)
  File "/usr/lib/python3.6/ssl.py", line 407, in wrap_socket
    _context=self, _session=session)
  File "/usr/lib/python3.6/ssl.py", line 817, in __init__
    self.do_handshake()
  File "/usr/lib/python3.6/ssl.py", line 1077, in do_handshake
    self._sslobj.do_handshake()
  File "/usr/lib/python3.6/ssl.py", line 689, in do_handshake
    self._sslobj.do_handshake()
ssl.SSLError: [SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:852)
```

## OS
- Linux Ubuntu 18.04



Discussion:

Resolving patch:
diff --git a/frappe/email/doctype/email_domain/email_domain.py b/frappe/email/doctype/email_domain/email_domain.py
--- a/frappe/email/doctype/email_domain/email_domain.py
+++ b/frappe/email/doctype/email_domain/email_domain.py
@@ -73,6 +73,7 @@ def on_update(self):
 				email_account.set("attachment_limit",self.attachment_limit)
 				email_account.set("smtp_server",self.smtp_server)
 				email_account.set("smtp_port",self.smtp_port)
+				email_account.set("incoming_port", self.incoming_port)
 				email_account.save()
 			except Exception as e:
 				frappe.msgprint(email_account.name)


Test patch:
diff --git a/frappe/email/doctype/email_domain/test_email_domain.py b/frappe/email/doctype/email_domain/test_email_domain.py
--- a/frappe/email/doctype/email_domain/test_email_domain.py
+++ b/frappe/email/doctype/email_domain/test_email_domain.py
@@ -5,8 +5,35 @@
 
 import frappe
 import unittest
+from frappe.test_runner import make_test_objects
 
-# test_records = frappe.get_test_records('Domain')
+test_records = frappe.get_test_records('Email Domain')
 
 class TestDomain(unittest.TestCase):
-	pass
+
+	def setUp(self):
+		make_test_objects('Email Domain', reset=True)
+
+	def tearDown(self):
+		frappe.delete_doc("Email Account", "Test")
+		frappe.delete_doc("Email Domain", "test.com")
+
+	def test_on_update(self):
+		mail_domain = frappe.get_doc("Email Domain", "test.com")
+		mail_account = frappe.get_doc("Email Account", "Test")
+
+		# Initially, incoming_port is different in domain and account
+		self.assertNotEqual(mail_account.incoming_port, mail_domain.incoming_port)
+		# Trigger update of accounts using this domain
+		mail_domain.on_update()
+		mail_account = frappe.get_doc("Email Account", "Test")
+		# After update, incoming_port in account should match the domain
+		self.assertEqual(mail_account.incoming_port, mail_domain.incoming_port)
+
+		# Also make sure that the other attributes match
+		self.assertEqual(mail_account.use_imap, mail_domain.use_imap)
+		self.assertEqual(mail_account.use_ssl, mail_domain.use_ssl)
+		self.assertEqual(mail_account.use_tls, mail_domain.use_tls)
+		self.assertEqual(mail_account.attachment_limit, mail_domain.attachment_limit)
+		self.assertEqual(mail_account.smtp_server, mail_domain.smtp_server)
+		self.assertEqual(mail_account.smtp_port, mail_domain.smtp_port)
diff --git a/frappe/email/doctype/email_domain/test_records.json b/frappe/email/doctype/email_domain/test_records.json
new file mode 100644
--- /dev/null
+++ b/frappe/email/doctype/email_domain/test_records.json
@@ -0,0 +1,30 @@
+[
+ {
+  "doctype": "Email Domain",
+  "domain_name": "test.com",
+  "email_id": "_test@test.com",
+  "email_server": "imap.test.com",
+  "use_imap": "imap.test.com",
+  "use_ssl": 1,
+  "use_tls": 1,
+  "incoming_port": "993",
+  "attachment_limit": "1",
+  "smtp_server": "smtp.test.com",
+  "smtp_port": "587"
+ },
+ {
+  "doctype": "Email Account",
+  "name": "_Test Email Account 1",
+  "enable_incoming": 1,
+  "email_id": "_test@test.com",
+  "domain": "test.com",
+  "email_server": "imap.test.com",
+  "use_imap": 1,
+  "use_ssl": 0,
+  "use_tls": 1,
+  "incoming_port": "143",
+  "attachment_limit": "1",
+  "smtp_server": "smtp.test.com",
+  "smtp_port": "587"
+ }
+]