Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions imageroot/actions/add-relay-rule/20add-relay-rule
Original file line number Diff line number Diff line change
@@ -54,13 +54,14 @@ try:
agent.set_status('validation-failed')
json.dump([{'field':'rule_subject', 'parameter':'rule_subject','value': rule_subject, 'error':'rule_subject_must_be_*'}], fp=sys.stdout)
sys.exit(5)
if rule_type != 'sender':
# Copy auth and transport settings to existing rules with the same host:port key
cur.execute("""UPDATE relayrules SET transport=:transport, tls=:mandatory_tls, username=:username, password=:password WHERE host=:host AND port=:port AND rule_type=:rule_type""", values)
cur.execute("""INSERT INTO relayrules (rule_type, rule_subject, transport, host, port, tls, username, password, enabled) VALUES (:rule_type, :rule_subject, :transport, :host, :port, :mandatory_tls, :username, :password, :enabled)""", values)

except Exception as err:
sdb.rollback()
agent.set_status('validation-failed')
json.dump([{'field':'rule_subject', 'parameter':'rule_subject','value': str(err), 'error':'sender_data_cannot_be_saved_in_db'}], fp=sys.stdout)
sys.exit(3)
raise
else:
sdb.commit()

88 changes: 27 additions & 61 deletions imageroot/actions/alter-relay-rule/20alter_relay
Original file line number Diff line number Diff line change
@@ -9,77 +9,43 @@ import json
import sys
import mail
import agent
from sqlite3 import IntegrityError

class RelayNotFoundError(Exception):
pass

request = json.load(sys.stdin)
updatedict = request.copy() # values for SQL statements
updatedict['tls'] = 'encrypt' if request['mandatory_tls'] else 'may'
updatedict['enabled'] = 1 if request['enabled'] else 0

sdb = mail.pcdb_connect()
rule_type = request['rule_type']
rule_subject = request['rule_subject']
username = request.get("username", "")
password = request.get("password", "")
host = request['host']
port = request['port']
mandatory_tls = request.get("mandatory_tls", False)

def get_password_from_database(rule_subject):
# Function to query the database for the password based on sender or recipient
sdb = mail.pcdb_connect()
cur = sdb.cursor()
cur.execute("SELECT password FROM relayrules WHERE rule_subject=?", (rule_subject,))
result = cur.fetchone()
sdb.close()
if result:
return result[0]
else:
agent.set_status('validation-failed')
json.dump([{'field':'test_rule_credentials','parameter':'test_rule_credentials','value':'cannot_retrieve_password_user_from_sqlite','error':'cannot_retrieve_password_user_from_sqlite'}],fp=sys.stdout, default=str)
sys.exit(3)
rulerecord = sdb.execute("SELECT * FROM relayrules WHERE rule_subject=? AND rule_type=?", (
request["rule_subject"],
request["rule_type"],
)).fetchone()
if rulerecord is None:
# Current rule was not found
agent.set_status('validation-failed')
json.dump([{'field': 'rule_subject', 'parameter': 'subject', 'value': request["rule_subject"], 'error':'relay_not_found'}], fp=sys.stdout)
sys.exit(2)

# password not changed, we need to retrieve it from the database
if request['username'] and password == '':
password = get_password_from_database(rule_subject)
# Authentication is enabled but the password was not changed: retrieve it
# from the database to retain the current value.
if request["username"] and request["password"] == '':
updatedict["password"] = rulerecord['password']

# probe we could connect to the smtp server and retrieve if we use smtp or smtps
probe_credentials = mail.probe_and_validate_smtp_credentials(username, password, host, port, mandatory_tls)
transport_is_smtps = probe_credentials['transport_smtps']
# validate and obtain the server transport:
probe_credentials = mail.probe_and_validate_smtp_credentials(request["username"], updatedict["password"], request["host"], request["port"], request["mandatory_tls"])
updatedict['transport'] = 'smtps' if probe_credentials['transport_smtps'] else 'smtp'

try:
cur = sdb.cursor()

if 'host' in request:
cur.execute("""UPDATE relayrules SET host=? WHERE rule_subject=?""", (request['host'], rule_subject))

if 'port' in request:
cur.execute("""UPDATE relayrules SET port=? WHERE rule_subject=?""", (request['port'], rule_subject))

if 'mandatory_tls' in request:
cur.execute("""UPDATE relayrules SET tls=? WHERE rule_subject=?""", ('encrypt' if request['mandatory_tls'] else 'may', rule_subject))
cur.execute("""UPDATE relayrules SET transport=? WHERE rule_subject=?""", ('smtps' if transport_is_smtps else 'smtp', rule_subject))

if 'username' in request:
cur.execute("""UPDATE relayrules SET username=? WHERE rule_subject=?""", (request['username'], rule_subject))

if 'password' in request:
cur.execute("""UPDATE relayrules SET password=? WHERE rule_subject=?""", (password, rule_subject))

if 'enabled' in request:
cur.execute("""UPDATE relayrules SET enabled=? WHERE rule_subject=?""", (1 if request['enabled'] else 0, rule_subject))

if cur.rowcount == 0:
raise RelayNotFoundError()

except RelayNotFoundError:
# Update the record with rule_subject key:
sdb.execute("""UPDATE relayrules SET host=:host, port=:port, tls=:tls, transport=:transport, username=:username, password=:password, enabled=:enabled WHERE rule_subject=:rule_subject""", updatedict)
# Copy auth and transport settings to existing rules with the same host:port key:
if rulerecord["rule_type"] != "sender":
sdb.execute("""UPDATE relayrules SET tls=:tls, transport=:transport, username=:username, password=:password WHERE (rule_type != 'sender' AND host=:host AND port=:port)""", updatedict)
except Exception as ex:
print(agent.SD_ERR+"Failed to update relayrules table.", ex, file=sys.stderr)
sdb.rollback()
agent.set_status('validation-failed')
json.dump([{'field': 'rule_subject', 'parameter': 'subject', 'value': rule_subject, 'error':'relay_not_found'}], fp=sys.stdout)
sys.exit(2)

raise
else:
sdb.commit()

finally:
sdb.close()
4 changes: 4 additions & 0 deletions imageroot/pypkg/mail.py
Original file line number Diff line number Diff line change
@@ -34,6 +34,10 @@ def pcdb_connect(readonly=False):
con = sqlite3.connect(uri, uri=True)
con.row_factory = sqlite3.Row
con.execute("""PRAGMA foreign_keys = ON""")
def dbg(statement):
print(agent.SD_DEBUG+statement, file=sys.stderr)
if os.getenv("DEBUG"):
con.set_trace_callback(dbg)
return con

def get_user_domains(rdb):
2 changes: 2 additions & 0 deletions postfix/etc/postfix/relaycredentials.cf
Original file line number Diff line number Diff line change
@@ -10,3 +10,5 @@ query = SELECT FORMAT('%%s:%%s', username, password) AS credentials
(rule_subject = '%s' AND rule_type = 'sender')
OR '%s' IN (FORMAT('[%%s]:%%s', host, port), FORMAT('[%%s]', host))
)
LIMIT 1
expansion_limit = 1
7 changes: 5 additions & 2 deletions ui/public/i18n/en/translation.json
Original file line number Diff line number Diff line change
@@ -392,6 +392,7 @@
"enabled": "Status",
"host": "Hostname",
"port": "Port",
"port_placeholder": "e.g. 25, 587 or 465",
"authentication": "Authentication",
"username": "Username",
"password": "Password",
@@ -402,9 +403,11 @@
"no_relay_rules_description": "There is no relay rules configured",
"col_has_password": "Authentication",
"col_rule_subject": "Rule subject",
"col_host": "Hostname",
"col_host": "Hostname and port",
"col_enabled": "Status",
"unchanged": "Unchanged",
"shared_credentials_info_title": "Shared host settings",
"shared_credentials_info_description": "Authentication and TLS settings for Default and Recipient rules are shared and applied collectively to all rules with the same Hostname and port.",
"settings": {
"networks_label": "Allow relay from these IP addresses",
"networks_helper": "One address per line, in IPv4/6 CIDR format",
@@ -415,7 +418,7 @@
"bcc": "Always BCC address",
"bcc_string_lte": "Invalid email format",
"is_bcc_enabled": "Always BCC",
"bcc_placeholder": "e.g. [email protected]",
"bcc_placeholder": "e.g. [email protected]",
"is_bcc_enabled_tooltips": "When enabled, the specified address will receive a copy of every message sent or received through this server. This setting is useful for mail archiving solutions."
},
"error": {
35 changes: 29 additions & 6 deletions ui/src/views/Relay.vue
Original file line number Diff line number Diff line change
@@ -22,6 +22,17 @@
</div>
</cv-column>
</cv-row>
<cv-row>
<cv-column>
<NsInlineNotification
v-if="hasWildcard || hasRulesByRecipient"
kind="info"
:title="$t('relay.shared_credentials_info_title')"
:description="$t('relay.shared_credentials_info_description')"
:showCloseButton="false"
/>
</cv-column>
</cv-row>
<cv-row>
<cv-column>
<NsInlineNotification
@@ -127,14 +138,17 @@
</p>
</cv-data-table-cell>
<cv-data-table-cell>
{{ row.host }}
{{ row.host }}:{{ row.port }}
</cv-data-table-cell>
<cv-data-table-cell>
{{
row.has_password
? $t("common.enabled")
: $t("common.disabled")
}}
<div v-if="row.has_password">
{{ row.username }}
</div>
<NsTag
v-else
kind="high-contrast"
:label="$t('common.disabled')"
></NsTag>
</cv-data-table-cell>
<cv-data-table-cell>
<NsTag
@@ -270,6 +284,7 @@
/>
<NsTextInput
v-model="form.port"
:placeholder="$t('relay.port_placeholder')"
:label="$t('relay.port')"
:invalid-message="error.form.port"
ref="form.port"
@@ -452,6 +467,14 @@ export default {
ruleTypeTranslation: function () {
return this.$t("relay." + this.form.rule_type);
},
hasRulesByRecipient: function () {
for (const row of this.relayRules) {
if (row.rule_type == "recipient") {
return true;
}
}
return false;
},
},
beforeRouteEnter(to, from, next) {
next((vm) => {