mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2024-11-22 02:17:26 +00:00
bidirectional alias controls: a new permitted_senders column in the aliases table allows setting who can send as an address independently of where the address forwards to
But the default permitted senders are the same as the addresses the alias forwards to. Merge branch 'dhpiggott-bidirectional-alias-controls'
This commit is contained in:
commit
8c08f957cd
@ -185,14 +185,15 @@ def mail_aliases():
|
|||||||
if request.args.get("format", "") == "json":
|
if request.args.get("format", "") == "json":
|
||||||
return json_response(get_mail_aliases_ex(env))
|
return json_response(get_mail_aliases_ex(env))
|
||||||
else:
|
else:
|
||||||
return "".join(x+"\t"+y+"\n" for x, y in get_mail_aliases(env))
|
return "".join(address+"\t"+receivers+"\t"+(senders or "")+"\n" for address, receivers, senders in get_mail_aliases(env))
|
||||||
|
|
||||||
@app.route('/mail/aliases/add', methods=['POST'])
|
@app.route('/mail/aliases/add', methods=['POST'])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only
|
||||||
def mail_aliases_add():
|
def mail_aliases_add():
|
||||||
return add_mail_alias(
|
return add_mail_alias(
|
||||||
request.form.get('source', ''),
|
request.form.get('address', ''),
|
||||||
request.form.get('destination', ''),
|
request.form.get('forwards_to', ''),
|
||||||
|
request.form.get('permitted_senders', ''),
|
||||||
env,
|
env,
|
||||||
update_if_exists=(request.form.get('update_if_exists', '') == '1')
|
update_if_exists=(request.form.get('update_if_exists', '') == '1')
|
||||||
)
|
)
|
||||||
@ -200,7 +201,7 @@ def mail_aliases_add():
|
|||||||
@app.route('/mail/aliases/remove', methods=['POST'])
|
@app.route('/mail/aliases/remove', methods=['POST'])
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only
|
||||||
def mail_aliases_remove():
|
def mail_aliases_remove():
|
||||||
return remove_mail_alias(request.form.get('source', ''), env)
|
return remove_mail_alias(request.form.get('address', ''), env)
|
||||||
|
|
||||||
@app.route('/mail/domains')
|
@app.route('/mail/domains')
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only
|
||||||
|
@ -181,13 +181,13 @@ def get_admins(env):
|
|||||||
return users
|
return users
|
||||||
|
|
||||||
def get_mail_aliases(env):
|
def get_mail_aliases(env):
|
||||||
# Returns a sorted list of tuples of (alias, forward-to string).
|
# Returns a sorted list of tuples of (address, forward-tos, permitted-senders).
|
||||||
c = open_database(env)
|
c = open_database(env)
|
||||||
c.execute('SELECT source, destination FROM aliases')
|
c.execute('SELECT source, destination, permitted_senders FROM aliases')
|
||||||
aliases = { row[0]: row[1] for row in c.fetchall() } # make dict
|
aliases = { row[0]: row for row in c.fetchall() } # make dict
|
||||||
|
|
||||||
# put in a canonical order: sort by domain, then by email address lexicographically
|
# put in a canonical order: sort by domain, then by email address lexicographically
|
||||||
aliases = [ (source, aliases[source]) for source in utils.sort_email_addresses(aliases.keys(), env) ]
|
aliases = [ aliases[address] for address in utils.sort_email_addresses(aliases.keys(), env) ]
|
||||||
return aliases
|
return aliases
|
||||||
|
|
||||||
def get_mail_aliases_ex(env):
|
def get_mail_aliases_ex(env):
|
||||||
@ -199,9 +199,10 @@ def get_mail_aliases_ex(env):
|
|||||||
# domain: "domain.tld",
|
# domain: "domain.tld",
|
||||||
# alias: [
|
# alias: [
|
||||||
# {
|
# {
|
||||||
# source: "name@domain.tld", # IDNA-encoded
|
# address: "name@domain.tld", # IDNA-encoded
|
||||||
# source_display: "name@domain.tld", # full Unicode
|
# address_display: "name@domain.tld", # full Unicode
|
||||||
# destination: ["target1@domain.com", "target2@domain.com", ...],
|
# forwards_to: ["user1@domain.com", "receiver-only1@domain.com", ...],
|
||||||
|
# permitted_senders: ["user1@domain.com", "sender-only1@domain.com", ...] OR null,
|
||||||
# required: True|False
|
# required: True|False
|
||||||
# },
|
# },
|
||||||
# ...
|
# ...
|
||||||
@ -212,10 +213,10 @@ def get_mail_aliases_ex(env):
|
|||||||
|
|
||||||
required_aliases = get_required_aliases(env)
|
required_aliases = get_required_aliases(env)
|
||||||
domains = {}
|
domains = {}
|
||||||
for source, destination in get_mail_aliases(env):
|
for address, forwards_to, permitted_senders in get_mail_aliases(env):
|
||||||
# get alias info
|
# get alias info
|
||||||
domain = get_domain(source)
|
domain = get_domain(address)
|
||||||
required = (source in required_aliases)
|
required = (address in required_aliases)
|
||||||
|
|
||||||
# add to list
|
# add to list
|
||||||
if not domain in domains:
|
if not domain in domains:
|
||||||
@ -224,18 +225,19 @@ def get_mail_aliases_ex(env):
|
|||||||
"aliases": [],
|
"aliases": [],
|
||||||
}
|
}
|
||||||
domains[domain]["aliases"].append({
|
domains[domain]["aliases"].append({
|
||||||
"source": source,
|
"address": address,
|
||||||
"source_display": prettify_idn_email_address(source),
|
"address_display": prettify_idn_email_address(address),
|
||||||
"destination": [prettify_idn_email_address(d.strip()) for d in destination.split(",")],
|
"forwards_to": [prettify_idn_email_address(r.strip()) for r in forwards_to.split(",")],
|
||||||
|
"permitted_senders": [prettify_idn_email_address(s.strip()) for s in permitted_senders.split(",")] if permitted_senders is not None else None,
|
||||||
"required": required,
|
"required": required,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Sort domains.
|
# Sort domains.
|
||||||
domains = [domains[domain] for domain in utils.sort_domains(domains.keys(), env)]
|
domains = [domains[domain] for domain in utils.sort_domains(domains.keys(), env)]
|
||||||
|
|
||||||
# Sort aliases within each domain first by required-ness then lexicographically by source address.
|
# Sort aliases within each domain first by required-ness then lexicographically by address.
|
||||||
for domain in domains:
|
for domain in domains:
|
||||||
domain["aliases"].sort(key = lambda alias : (alias["required"], alias["source"]))
|
domain["aliases"].sort(key = lambda alias : (alias["required"], alias["address"]))
|
||||||
return domains
|
return domains
|
||||||
|
|
||||||
def get_domain(emailaddr, as_unicode=True):
|
def get_domain(emailaddr, as_unicode=True):
|
||||||
@ -249,8 +251,8 @@ def get_mail_domains(env, filter_aliases=lambda alias : True):
|
|||||||
# Returns the domain names (IDNA-encoded) of all of the email addresses
|
# Returns the domain names (IDNA-encoded) of all of the email addresses
|
||||||
# configured on the system.
|
# configured on the system.
|
||||||
return set(
|
return set(
|
||||||
[get_domain(addr, as_unicode=False) for addr in get_mail_users(env)]
|
[get_domain(login, as_unicode=False) for login in get_mail_users(env)]
|
||||||
+ [get_domain(source, as_unicode=False) for source, target in get_mail_aliases(env) if filter_aliases((source, target)) ]
|
+ [get_domain(address, as_unicode=False) for address, *_ in get_mail_aliases(env) if filter_aliases(address) ]
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_mail_user(email, pw, privs, env):
|
def add_mail_user(email, pw, privs, env):
|
||||||
@ -406,67 +408,91 @@ def add_remove_mail_user_privilege(email, priv, action, env):
|
|||||||
|
|
||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
def add_mail_alias(source, destination, env, update_if_exists=False, do_kick=True):
|
def add_mail_alias(address, forwards_to, permitted_senders, env, update_if_exists=False, do_kick=True):
|
||||||
# convert Unicode domain to IDNA
|
# convert Unicode domain to IDNA
|
||||||
source = sanitize_idn_email_address(source)
|
address = sanitize_idn_email_address(address)
|
||||||
|
|
||||||
# Our database is case sensitive (oops), which affects mail delivery
|
# Our database is case sensitive (oops), which affects mail delivery
|
||||||
# (Postfix always queries in lowercase?), so force lowercase.
|
# (Postfix always queries in lowercase?), so force lowercase.
|
||||||
source = source.lower()
|
address = address.lower()
|
||||||
|
|
||||||
# validate source
|
# validate address
|
||||||
source = source.strip()
|
address = address.strip()
|
||||||
if source == "":
|
if address == "":
|
||||||
return ("No incoming email address provided.", 400)
|
return ("No email address provided.", 400)
|
||||||
if not validate_email(source, mode='alias'):
|
if not validate_email(address, mode='alias'):
|
||||||
return ("Invalid incoming email address (%s)." % source, 400)
|
return ("Invalid email address (%s)." % address, 400)
|
||||||
|
|
||||||
|
# validate forwards_to
|
||||||
|
validated_forwards_to = []
|
||||||
|
forwards_to = forwards_to.strip()
|
||||||
|
|
||||||
# extra checks for email addresses used in domain control validation
|
# extra checks for email addresses used in domain control validation
|
||||||
is_dcv_source = is_dcv_address(source)
|
is_dcv_source = is_dcv_address(address)
|
||||||
|
|
||||||
# validate destination
|
|
||||||
dests = []
|
|
||||||
destination = destination.strip()
|
|
||||||
|
|
||||||
# Postfix allows a single @domain.tld as the destination, which means
|
# Postfix allows a single @domain.tld as the destination, which means
|
||||||
# the local part on the address is preserved in the rewrite. We must
|
# the local part on the address is preserved in the rewrite. We must
|
||||||
# try to convert Unicode to IDNA first before validating that it's a
|
# try to convert Unicode to IDNA first before validating that it's a
|
||||||
# legitimate alias address. Don't allow this sort of rewriting for
|
# legitimate alias address. Don't allow this sort of rewriting for
|
||||||
# DCV source addresses.
|
# DCV source addresses.
|
||||||
d1 = sanitize_idn_email_address(destination)
|
r1 = sanitize_idn_email_address(forwards_to)
|
||||||
if validate_email(d1, mode='alias') and not is_dcv_source:
|
if validate_email(r1, mode='alias') and not is_dcv_source:
|
||||||
dests.append(d1)
|
validated_forwards_to.append(r1)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Parse comma and \n-separated destination emails & validate. In this
|
# Parse comma and \n-separated destination emails & validate. In this
|
||||||
# case, the recipients must be complete email addresses.
|
# case, the forwards_to must be complete email addresses.
|
||||||
for line in destination.split("\n"):
|
for line in forwards_to.split("\n"):
|
||||||
for email in line.split(","):
|
for email in line.split(","):
|
||||||
email = email.strip()
|
email = email.strip()
|
||||||
if email == "": continue
|
if email == "": continue
|
||||||
email = sanitize_idn_email_address(email) # Unicode => IDNA
|
email = sanitize_idn_email_address(email) # Unicode => IDNA
|
||||||
if not validate_email(email):
|
if not validate_email(email):
|
||||||
return ("Invalid destination email address (%s)." % email, 400)
|
return ("Invalid receiver email address (%s)." % email, 400)
|
||||||
if is_dcv_source and not is_dcv_address(email) and "admin" not in get_mail_user_privileges(email, env, empty_on_error=True):
|
if is_dcv_source and not is_dcv_address(email) and "admin" not in get_mail_user_privileges(email, env, empty_on_error=True):
|
||||||
# Make domain control validation hijacking a little harder to mess up by
|
# Make domain control validation hijacking a little harder to mess up by
|
||||||
# requiring aliases for email addresses typically used in DCV to forward
|
# requiring aliases for email addresses typically used in DCV to forward
|
||||||
# only to accounts that are administrators on this system.
|
# only to accounts that are administrators on this system.
|
||||||
return ("This alias can only have administrators of this system as destinations because the address is frequently used for domain control validation.", 400)
|
return ("This alias can only have administrators of this system as destinations because the address is frequently used for domain control validation.", 400)
|
||||||
dests.append(email)
|
validated_forwards_to.append(email)
|
||||||
if len(destination) == 0:
|
|
||||||
return ("No destination email address(es) provided.", 400)
|
# validate permitted_senders
|
||||||
destination = ",".join(dests)
|
valid_logins = get_mail_users(env)
|
||||||
|
validated_permitted_senders = []
|
||||||
|
permitted_senders = permitted_senders.strip()
|
||||||
|
|
||||||
|
# Parse comma and \n-separated sender logins & validate. The permitted_senders must be
|
||||||
|
# valid usernames.
|
||||||
|
for line in permitted_senders.split("\n"):
|
||||||
|
for login in line.split(","):
|
||||||
|
login = login.strip()
|
||||||
|
if login == "": continue
|
||||||
|
if login not in valid_logins:
|
||||||
|
return ("Invalid permitted sender: %s is not a user on this system." % login, 400)
|
||||||
|
validated_permitted_senders.append(login)
|
||||||
|
|
||||||
|
# Make sure the alias has either a forwards_to or a permitted_sender.
|
||||||
|
if len(validated_forwards_to) + len(validated_permitted_senders) == 0:
|
||||||
|
return ("The alias must either forward to an address or have a permitted sender.", 400)
|
||||||
|
|
||||||
# save to db
|
# save to db
|
||||||
|
|
||||||
|
forwards_to = ",".join(validated_forwards_to)
|
||||||
|
|
||||||
|
if len(validated_permitted_senders) == 0:
|
||||||
|
permitted_senders = None
|
||||||
|
else:
|
||||||
|
permitted_senders = ",".join(validated_permitted_senders)
|
||||||
|
|
||||||
conn, c = open_database(env, with_connection=True)
|
conn, c = open_database(env, with_connection=True)
|
||||||
try:
|
try:
|
||||||
c.execute("INSERT INTO aliases (source, destination) VALUES (?, ?)", (source, destination))
|
c.execute("INSERT INTO aliases (source, destination, permitted_senders) VALUES (?, ?, ?)", (address, forwards_to, permitted_senders))
|
||||||
return_status = "alias added"
|
return_status = "alias added"
|
||||||
except sqlite3.IntegrityError:
|
except sqlite3.IntegrityError:
|
||||||
if not update_if_exists:
|
if not update_if_exists:
|
||||||
return ("Alias already exists (%s)." % source, 400)
|
return ("Alias already exists (%s)." % address, 400)
|
||||||
else:
|
else:
|
||||||
c.execute("UPDATE aliases SET destination = ? WHERE source = ?", (destination, source))
|
c.execute("UPDATE aliases SET destination = ?, permitted_senders = ? WHERE source = ?", (forwards_to, permitted_senders, address))
|
||||||
return_status = "alias updated"
|
return_status = "alias updated"
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@ -475,15 +501,15 @@ def add_mail_alias(source, destination, env, update_if_exists=False, do_kick=Tru
|
|||||||
# Update things in case any new domains are added.
|
# Update things in case any new domains are added.
|
||||||
return kick(env, return_status)
|
return kick(env, return_status)
|
||||||
|
|
||||||
def remove_mail_alias(source, env, do_kick=True):
|
def remove_mail_alias(address, env, do_kick=True):
|
||||||
# convert Unicode domain to IDNA
|
# convert Unicode domain to IDNA
|
||||||
source = sanitize_idn_email_address(source)
|
address = sanitize_idn_email_address(address)
|
||||||
|
|
||||||
# remove
|
# remove
|
||||||
conn, c = open_database(env, with_connection=True)
|
conn, c = open_database(env, with_connection=True)
|
||||||
c.execute("DELETE FROM aliases WHERE source=?", (source,))
|
c.execute("DELETE FROM aliases WHERE source=?", (address,))
|
||||||
if c.rowcount != 1:
|
if c.rowcount != 1:
|
||||||
return ("That's not an alias (%s)." % source, 400)
|
return ("That's not an alias (%s)." % address, 400)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
if do_kick:
|
if do_kick:
|
||||||
@ -507,8 +533,8 @@ def get_required_aliases(env):
|
|||||||
# email on that domain are the required aliases or a catch-all/domain-forwarder.
|
# email on that domain are the required aliases or a catch-all/domain-forwarder.
|
||||||
real_mail_domains = get_mail_domains(env,
|
real_mail_domains = get_mail_domains(env,
|
||||||
filter_aliases = lambda alias :
|
filter_aliases = lambda alias :
|
||||||
not alias[0].startswith("postmaster@") and not alias[0].startswith("admin@")
|
not alias.startswith("postmaster@") and not alias.startswith("admin@")
|
||||||
and not alias[0].startswith("@")
|
and not alias.startswith("@")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create postmaster@ and admin@ for all domains we serve mail on.
|
# Create postmaster@ and admin@ for all domains we serve mail on.
|
||||||
@ -535,34 +561,34 @@ def kick(env, mail_result=None):
|
|||||||
existing_aliases = get_mail_aliases(env)
|
existing_aliases = get_mail_aliases(env)
|
||||||
required_aliases = get_required_aliases(env)
|
required_aliases = get_required_aliases(env)
|
||||||
|
|
||||||
def ensure_admin_alias_exists(source):
|
def ensure_admin_alias_exists(address):
|
||||||
# If a user account exists with that address, we're good.
|
# If a user account exists with that address, we're good.
|
||||||
if source in existing_users:
|
if address in existing_users:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Does this alias exists?
|
# Does this alias exists?
|
||||||
for s, t in existing_aliases:
|
for a, *_ in existing_aliases:
|
||||||
if s == source:
|
if a == address:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Doesn't exist.
|
# Doesn't exist.
|
||||||
administrator = get_system_administrator(env)
|
administrator = get_system_administrator(env)
|
||||||
if source == administrator: return # don't make an alias from the administrator to itself --- this alias must be created manually
|
if address == administrator: return # don't make an alias from the administrator to itself --- this alias must be created manually
|
||||||
add_mail_alias(source, administrator, env, do_kick=False)
|
add_mail_alias(address, administrator, "", env, do_kick=False)
|
||||||
results.append("added alias %s (=> %s)\n" % (source, administrator))
|
results.append("added alias %s (<==> %s)\n" % (address, administrator))
|
||||||
|
|
||||||
for alias in required_aliases:
|
for address in required_aliases:
|
||||||
ensure_admin_alias_exists(alias)
|
ensure_admin_alias_exists(address)
|
||||||
|
|
||||||
# Remove auto-generated postmaster/admin on domains we no
|
# Remove auto-generated postmaster/admin on domains we no
|
||||||
# longer have any other email addresses for.
|
# longer have any other email addresses for.
|
||||||
for source, target in existing_aliases:
|
for address, forwards_to, *_ in existing_aliases:
|
||||||
user, domain = source.split("@")
|
user, domain = address.split("@")
|
||||||
if user in ("postmaster", "admin") \
|
if user in ("postmaster", "admin") \
|
||||||
and source not in required_aliases \
|
and address not in required_aliases \
|
||||||
and target == get_system_administrator(env):
|
and forwards_to == get_system_administrator(env):
|
||||||
remove_mail_alias(source, env, do_kick=False)
|
remove_mail_alias(address, env, do_kick=False)
|
||||||
results.append("removed alias %s (was to %s; domain no longer used for email)\n" % (source, target))
|
results.append("removed alias %s (was to %s; domain no longer used for email)\n" % (address, forwards_to))
|
||||||
|
|
||||||
# Update DNS and nginx in case any domains are added/removed.
|
# Update DNS and nginx in case any domains are added/removed.
|
||||||
|
|
||||||
|
@ -351,11 +351,14 @@ def check_primary_hostname_dns(domain, env, output, dns_domains, dns_zonefiles):
|
|||||||
check_alias_exists("Hostmaster contact address", "hostmaster@" + domain, env, output)
|
check_alias_exists("Hostmaster contact address", "hostmaster@" + domain, env, output)
|
||||||
|
|
||||||
def check_alias_exists(alias_name, alias, env, output):
|
def check_alias_exists(alias_name, alias, env, output):
|
||||||
mail_alises = dict(get_mail_aliases(env))
|
mail_aliases = dict([(address, receivers) for address, receivers, *_ in get_mail_aliases(env)])
|
||||||
if alias in mail_alises:
|
if alias in mail_aliases:
|
||||||
output.print_ok("%s exists as a mail alias. [%s ↦ %s]" % (alias_name, alias, mail_alises[alias]))
|
if mail_aliases[alias]:
|
||||||
|
output.print_ok("%s exists as a mail alias. [%s ↦ %s]" % (alias_name, alias, mail_aliases[alias]))
|
||||||
else:
|
else:
|
||||||
output.print_error("""You must add a mail alias for %s and direct email to you or another administrator.""" % alias)
|
output.print_error("""You must set the destination of the mail alias for %s to direct email to you or another administrator.""" % alias)
|
||||||
|
else:
|
||||||
|
output.print_error("""You must add a mail alias for %s which directs email to you or another administrator.""" % alias)
|
||||||
|
|
||||||
def check_dns_zone(domain, env, output, dns_zonefiles):
|
def check_dns_zone(domain, env, output, dns_zonefiles):
|
||||||
# If a DS record is set at the registrar, check DNSSEC first because it will affect the NS query.
|
# If a DS record is set at the registrar, check DNSSEC first because it will affect the NS query.
|
||||||
@ -492,7 +495,7 @@ def check_mail_domain(domain, env, output):
|
|||||||
|
|
||||||
# Check that the postmaster@ email address exists. Not required if the domain has a
|
# Check that the postmaster@ email address exists. Not required if the domain has a
|
||||||
# catch-all address or domain alias.
|
# catch-all address or domain alias.
|
||||||
if "@" + domain not in dict(get_mail_aliases(env)):
|
if "@" + domain not in [address for address, *_ in get_mail_aliases(env)]:
|
||||||
check_alias_exists("Postmaster contact address", "postmaster@" + domain, env, output)
|
check_alias_exists("Postmaster contact address", "postmaster@" + domain, env, output)
|
||||||
|
|
||||||
# Stop if the domain is listed in the Spamhaus Domain Block List.
|
# Stop if the domain is listed in the Spamhaus Domain Block List.
|
||||||
|
@ -9,31 +9,59 @@
|
|||||||
|
|
||||||
<p>Aliases are email forwarders. An alias can forward email to a <a href="#" onclick="return show_panel('users')">mail user</a> or to any email address.</p>
|
<p>Aliases are email forwarders. An alias can forward email to a <a href="#" onclick="return show_panel('users')">mail user</a> or to any email address.</p>
|
||||||
|
|
||||||
<form class="form-horizontal" role="form" onsubmit="do_add_alias(); return false;">
|
<p>To use an alias or any address besides your own login username in outbound mail, the sending user must be included as a permitted sender for the alias.</p>
|
||||||
|
|
||||||
|
<form id="addalias-form" class="form-horizontal" role="form" onsubmit="do_add_alias(); return false;">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-offset-1 col-sm-11">
|
<div class="col-sm-offset-1 col-sm-11">
|
||||||
<div id="alias_type_buttons" class="btn-group btn-group-xs">
|
<div id="alias_type_buttons" class="btn-group btn-group-xs">
|
||||||
<button type="button" class="btn btn-default active" data-mode="regular">Regular</button>
|
<button type="button" class="btn btn-default" data-mode="regular">Regular</button>
|
||||||
<button type="button" class="btn btn-default" data-mode="catchall">Catch-All</button>
|
<button type="button" class="btn btn-default" data-mode="catchall">Catch-All</button>
|
||||||
<button type="button" class="btn btn-default" data-mode="domainalias">Domain Alias</button>
|
<button type="button" class="btn btn-default" data-mode="domainalias">Domain Alias</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="alias_mode_info" class="text-info small" style="display: none; margin: .5em 0 0 0;">
|
<div id="alias_mode_info" class="text-info small" style="display: none; margin: .5em 0 0 0;">
|
||||||
<span class="catchall hidden">A catch-all alias captures all otherwise unmatched email to a domain. Enter just a part of an email address starting with the @-sign.</span>
|
<span class="catchall hidden">A catch-all alias captures all otherwise unmatched email to a domain.</span>
|
||||||
<span class="domainalias hidden">A domain alias forwards all otherwise unmatched mail from one domain to another domain, preserving the part before the @-sign.</span>
|
<span class="domainalias hidden">A domain alias forwards all otherwise unmatched email from one domain to another domain, preserving the part before the @-sign.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="addaliasEmail" class="col-sm-1 control-label">Alias</label>
|
<label for="addaliasAddress" class="col-sm-1 control-label">Alias</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<input type="email" class="form-control" id="addaliasEmail">
|
<input type="email" class="form-control" id="addaliasAddress">
|
||||||
<div style="margin-top: 3px; padding-left: 3px; font-size: 90%" class="text-muted">You may use international (non-ASCII) characters for the domain part of the email address only.</div>
|
<div style="margin-top: 3px; padding-left: 3px; font-size: 90%" class="text-muted">
|
||||||
|
<span class="catchall domainalias">Enter just the part of an email address starting with the @-sign.</span>
|
||||||
|
You may use international (non-ASCII) characters for the domain part of the email address only.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="addaliasTargets" class="col-sm-1 control-label">Forward To</label>
|
<label for="addaliasForwardsTo" class="col-sm-1 control-label">Forwards To</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<textarea class="form-control" rows="3" id="addaliasTargets"></textarea>
|
<textarea class="form-control" rows="3" id="addaliasForwardsTo"></textarea>
|
||||||
|
<div style="margin-top: 3px; padding-left: 3px; font-size: 90%" class="text-muted">
|
||||||
|
<span class="domainalias">Enter just the part of an email address starting with the @-sign.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="addaliasSenders" class="col-sm-1 control-label">Permitted Senders</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<div class="radio">
|
||||||
|
<label>
|
||||||
|
<input id="addaliasForwardsToNotAdvanced" name="addaliasForwardsToDivToggle" type="radio" checked onclick="$('#addaliasForwardsToDiv').toggle(false)">
|
||||||
|
Any mail user listed in the Fowards To box can send mail claiming to be from <span class="regularalias">the alias address</span><span class="catchall domainalias">any address on the alias domain</span>.
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="radio">
|
||||||
|
<label>
|
||||||
|
<input id="addaliasForwardsToAdvanced" name="addaliasForwardsToDivToggle" type="radio" id="addaliasForwardsToDivShower" onclick="$('#addaliasForwardsToDiv').toggle(true)">
|
||||||
|
I’ll enter the mail users that can send mail claiming to be from <span class="regularalias">the alias address</span><span class="catchall domainalias">any address on the alias domain</span>.
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div id="addaliasForwardsToDiv" style="margin-top: .5em; margin-left: 1.4em; display: none;">
|
||||||
|
<textarea class="form-control" rows="3" id="addaliasSenders" placeholder="one user per line or separated by commas"></textarea>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@ -51,6 +79,7 @@
|
|||||||
<th></th>
|
<th></th>
|
||||||
<th>Alias<br></th>
|
<th>Alias<br></th>
|
||||||
<th>Forwards To</th>
|
<th>Forwards To</th>
|
||||||
|
<th>Permitted Senders</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -70,8 +99,9 @@
|
|||||||
<span class="glyphicon glyphicon-trash"></span>
|
<span class="glyphicon glyphicon-trash"></span>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class='email'> </td>
|
<td class='address'> </td>
|
||||||
<td class='target'> </td>
|
<td class='forwardsTo'> </td>
|
||||||
|
<td class='senders'> </td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -98,10 +128,12 @@ function show_aliases() {
|
|||||||
n.attr('id', '');
|
n.attr('id', '');
|
||||||
|
|
||||||
if (alias.required) n.addClass('alias-required');
|
if (alias.required) n.addClass('alias-required');
|
||||||
n.attr('data-email', alias.source_display); // this is decoded from IDNA, but will get re-coded to IDNA on the backend
|
n.attr('data-address', alias.address_display); // this is decoded from IDNA, but will get re-coded to IDNA on the backend
|
||||||
n.find('td.email').text(alias.source_display)
|
n.find('td.address').text(alias.address_display)
|
||||||
for (var j = 0; j < alias.destination.length; j++)
|
for (var j = 0; j < alias.forwards_to.length; j++)
|
||||||
n.find('td.target').append($("<div></div>").text(alias.destination[j]))
|
n.find('td.forwardsTo').append($("<div></div>").text(alias.forwards_to[j]))
|
||||||
|
for (var j = 0; j < (alias.permitted_senders ? alias.permitted_senders.length : 0); j++)
|
||||||
|
n.find('td.senders').append($("<div></div>").text(alias.permitted_senders[j]))
|
||||||
$('#alias_table tbody').append(n);
|
$('#alias_table tbody').append(n);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -111,25 +143,25 @@ function show_aliases() {
|
|||||||
$('#alias_type_buttons button').off('click').click(function() {
|
$('#alias_type_buttons button').off('click').click(function() {
|
||||||
$('#alias_type_buttons button').removeClass('active');
|
$('#alias_type_buttons button').removeClass('active');
|
||||||
$(this).addClass('active');
|
$(this).addClass('active');
|
||||||
|
$('#addalias-form .regularalias, #addalias-form .catchall, #addalias-form .domainalias').addClass('hidden');
|
||||||
if ($(this).attr('data-mode') == "regular") {
|
if ($(this).attr('data-mode') == "regular") {
|
||||||
$('#addaliasEmail').attr('type', 'email');
|
$('#addaliasAddress').attr('type', 'email');
|
||||||
$('#addaliasEmail').attr('placeholder', 'incoming email address (e.g. you@yourdomain.com)');
|
$('#addaliasAddress').attr('placeholder', 'you@yourdomain.com (incoming email address)');
|
||||||
$('#addaliasTargets').attr('placeholder', 'forward to these email addresses (one per line or separated by commas)');
|
$('#addaliasForwardsTo').attr('placeholder', 'one address per line or separated by commas');
|
||||||
$('#alias_mode_info').slideUp();
|
$('#alias_mode_info').slideUp();
|
||||||
|
$('#addalias-form .regularalias').removeClass('hidden');
|
||||||
} else if ($(this).attr('data-mode') == "catchall") {
|
} else if ($(this).attr('data-mode') == "catchall") {
|
||||||
$('#addaliasEmail').attr('type', 'text');
|
$('#addaliasAddress').attr('type', 'text');
|
||||||
$('#addaliasEmail').attr('placeholder', 'incoming catch-all address (e.g. @yourdomain.com)');
|
$('#addaliasAddress').attr('placeholder', '@yourdomain.com (incoming catch-all domain)');
|
||||||
$('#addaliasTargets').attr('placeholder', 'forward to these email addresses (one per line or separated by commas)');
|
$('#addaliasForwardsTo').attr('placeholder', 'one address per line or separated by commas');
|
||||||
$('#alias_mode_info').slideDown();
|
$('#alias_mode_info').slideDown();
|
||||||
$('#alias_mode_info span').addClass('hidden');
|
$('#addalias-form .catchall').removeClass('hidden');
|
||||||
$('#alias_mode_info span.catchall').removeClass('hidden');
|
|
||||||
} else if ($(this).attr('data-mode') == "domainalias") {
|
} else if ($(this).attr('data-mode') == "domainalias") {
|
||||||
$('#addaliasEmail').attr('type', 'text');
|
$('#addaliasAddress').attr('type', 'text');
|
||||||
$('#addaliasEmail').attr('placeholder', 'incoming domain (@yourdomain.com)');
|
$('#addaliasAddress').attr('placeholder', '@yourdomain.com (incoming catch-all domain)');
|
||||||
$('#addaliasTargets').attr('placeholder', 'forward to domain (@yourdomain.com)');
|
$('#addaliasForwardsTo').attr('placeholder', '@otherdomain.com (forward to other domain)');
|
||||||
$('#alias_mode_info').slideDown();
|
$('#alias_mode_info').slideDown();
|
||||||
$('#alias_mode_info span').addClass('hidden');
|
$('#addalias-form .domainalias').removeClass('hidden');
|
||||||
$('#alias_mode_info span.domainalias').removeClass('hidden');
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
$('#alias_type_buttons button[data-mode="regular"]').click(); // init
|
$('#alias_type_buttons button[data-mode="regular"]').click(); // init
|
||||||
@ -139,15 +171,21 @@ function show_aliases() {
|
|||||||
var is_alias_add_update = false;
|
var is_alias_add_update = false;
|
||||||
function do_add_alias() {
|
function do_add_alias() {
|
||||||
var title = (!is_alias_add_update) ? "Add Alias" : "Update Alias";
|
var title = (!is_alias_add_update) ? "Add Alias" : "Update Alias";
|
||||||
var email = $("#addaliasEmail").val();
|
var form_address = $("#addaliasAddress").val();
|
||||||
var targets = $("#addaliasTargets").val();
|
var form_forwardsto = $("#addaliasForwardsTo").val();
|
||||||
|
var form_senders = ($('#addaliasForwardsToAdvanced').prop('checked') ? $("#addaliasSenders").val() : '');
|
||||||
|
if ($('#addaliasForwardsToAdvanced').prop('checked') && !/\S/.exec($("#addaliasSenders").val())) {
|
||||||
|
show_modal_error(title, "You did not enter any permitted senders.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
api(
|
api(
|
||||||
"/mail/aliases/add",
|
"/mail/aliases/add",
|
||||||
"POST",
|
"POST",
|
||||||
{
|
{
|
||||||
update_if_exists: is_alias_add_update ? '1' : '0',
|
update_if_exists: is_alias_add_update ? '1' : '0',
|
||||||
source: email,
|
address: form_address,
|
||||||
destination: targets
|
forwards_to: form_forwardsto,
|
||||||
|
permitted_senders: form_senders
|
||||||
},
|
},
|
||||||
function(r) {
|
function(r) {
|
||||||
// Responses are multiple lines of pre-formatted text.
|
// Responses are multiple lines of pre-formatted text.
|
||||||
@ -162,48 +200,55 @@ function do_add_alias() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function aliases_reset_form() {
|
function aliases_reset_form() {
|
||||||
$("#addaliasEmail").prop('disabled', false);
|
$("#addaliasAddress").prop('disabled', false);
|
||||||
$("#addaliasEmail").val('')
|
$("#addaliasAddress").val('')
|
||||||
$("#addaliasTargets").val('')
|
$("#addaliasForwardsTo").val('')
|
||||||
|
$("#addaliasSenders").val('')
|
||||||
$('#alias-cancel').addClass('hidden');
|
$('#alias-cancel').addClass('hidden');
|
||||||
$('#add-alias-button').text('Add Alias');
|
$('#add-alias-button').text('Add Alias');
|
||||||
is_alias_add_update = false;
|
is_alias_add_update = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function aliases_edit(elem) {
|
function aliases_edit(elem) {
|
||||||
var email = $(elem).parents('tr').attr('data-email');
|
var address = $(elem).parents('tr').attr('data-address');
|
||||||
var targetdivs = $(elem).parents('tr').find('.target div');
|
var receiverdivs = $(elem).parents('tr').find('.forwardsTo div');
|
||||||
var targets = "";
|
var senderdivs = $(elem).parents('tr').find('.senders div');
|
||||||
for (var i = 0; i < targetdivs.length; i++)
|
var forwardsTo = "";
|
||||||
targets += $(targetdivs[i]).text() + "\n";
|
for (var i = 0; i < receiverdivs.length; i++)
|
||||||
|
forwardsTo += $(receiverdivs[i]).text() + "\n";
|
||||||
is_alias_add_update = true;
|
var senders = "";
|
||||||
$('#alias-cancel').removeClass('hidden');
|
for (var i = 0; i < senderdivs.length; i++)
|
||||||
$("#addaliasEmail").prop('disabled', true);
|
senders += $(senderdivs[i]).text() + "\n";
|
||||||
$("#addaliasEmail").val(email);
|
if (address.charAt(0) == '@' && forwardsTo.charAt(0) == '@')
|
||||||
$("#addaliasTargets").val(targets);
|
|
||||||
$('#add-alias-button').text('Update');
|
|
||||||
if (email.charAt(0) == '@' && targets.charAt(0) == '@')
|
|
||||||
$('#alias_type_buttons button[data-mode="domainalias"]').click();
|
$('#alias_type_buttons button[data-mode="domainalias"]').click();
|
||||||
else if (email.charAt(0) == '@')
|
else if (address.charAt(0) == '@')
|
||||||
$('#alias_type_buttons button[data-mode="catchall"]').click();
|
$('#alias_type_buttons button[data-mode="catchall"]').click();
|
||||||
else
|
else
|
||||||
$('#alias_type_buttons button[data-mode="regular"]').click();
|
$('#alias_type_buttons button[data-mode="regular"]').click();
|
||||||
|
$('#alias-cancel').removeClass('hidden');
|
||||||
|
$("#addaliasAddress").prop('disabled', true);
|
||||||
|
$("#addaliasAddress").val(address);
|
||||||
|
$("#addaliasForwardsTo").val(forwardsTo);
|
||||||
|
$('#addaliasForwardsToAdvanced').prop('checked', senders != "");
|
||||||
|
$('#addaliasForwardsToNotAdvanced').prop('checked', senders == "");
|
||||||
|
$("#addaliasSenders").val(senders);
|
||||||
|
$('#add-alias-button').text('Update');
|
||||||
$('body').animate({ scrollTop: 0 })
|
$('body').animate({ scrollTop: 0 })
|
||||||
|
is_alias_add_update = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function aliases_remove(elem) {
|
function aliases_remove(elem) {
|
||||||
var email = $(elem).parents('tr').attr('data-email');
|
var row_address = $(elem).parents('tr').attr('data-address');
|
||||||
show_modal_confirm(
|
show_modal_confirm(
|
||||||
"Remove Alias",
|
"Remove Alias",
|
||||||
"Remove " + email + "?",
|
"Remove " + row_address + "?",
|
||||||
"Remove",
|
"Remove",
|
||||||
function() {
|
function() {
|
||||||
api(
|
api(
|
||||||
"/mail/aliases/remove",
|
"/mail/aliases/remove",
|
||||||
"POST",
|
"POST",
|
||||||
{
|
{
|
||||||
source: email
|
address: row_address
|
||||||
},
|
},
|
||||||
function(r) {
|
function(r) {
|
||||||
// Responses are multiple lines of pre-formatted text.
|
// Responses are multiple lines of pre-formatted text.
|
||||||
|
@ -21,7 +21,7 @@ db_path=$STORAGE_ROOT/mail/users.sqlite
|
|||||||
if [ ! -f $db_path ]; then
|
if [ ! -f $db_path ]; then
|
||||||
echo Creating new user database: $db_path;
|
echo Creating new user database: $db_path;
|
||||||
echo "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, password TEXT NOT NULL, extra, privileges TEXT NOT NULL DEFAULT '');" | sqlite3 $db_path;
|
echo "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, password TEXT NOT NULL, extra, privileges TEXT NOT NULL DEFAULT '');" | sqlite3 $db_path;
|
||||||
echo "CREATE TABLE aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL);" | sqlite3 $db_path;
|
echo "CREATE TABLE aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);" | sqlite3 $db_path;
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ### User Authentication
|
# ### User Authentication
|
||||||
@ -71,18 +71,23 @@ tools/editconf.py /etc/postfix/main.cf \
|
|||||||
|
|
||||||
# ### Sender Validation
|
# ### Sender Validation
|
||||||
|
|
||||||
# Use a Sqlite3 database to set login maps. This is used with
|
# We use Postfix's reject_authenticated_sender_login_mismatch filter to
|
||||||
# reject_authenticated_sender_login_mismatch to see if user is
|
# prevent intra-domain spoofing by logged in but untrusted users in outbound
|
||||||
# allowed to send mail using FROM field specified in the request.
|
# email. In all outbound mail (the sender has authenticated), the MAIL FROM
|
||||||
|
# address (aka envelope or return path address) must be "owned" by the user
|
||||||
|
# who authenticated. An SQL query will find who are the owners of any given
|
||||||
|
# address.
|
||||||
tools/editconf.py /etc/postfix/main.cf \
|
tools/editconf.py /etc/postfix/main.cf \
|
||||||
smtpd_sender_login_maps=sqlite:/etc/postfix/sender-login-maps.cf
|
smtpd_sender_login_maps=sqlite:/etc/postfix/sender-login-maps.cf
|
||||||
|
|
||||||
# SQL statement to set login map which includes the case when user is
|
# Postfix will query the exact address first, where the priority will be alias
|
||||||
# sending email using a valid alias.
|
# records first, then user records. If there are no matches for the exact
|
||||||
# This is the same as virtual-alias-maps.cf, See below
|
# address, then Postfix will query just the domain part, which we call
|
||||||
|
# catch-alls and domain aliases. A NULL permitted_senders column means to
|
||||||
|
# take the value from the destination column.
|
||||||
cat > /etc/postfix/sender-login-maps.cf << EOF;
|
cat > /etc/postfix/sender-login-maps.cf << EOF;
|
||||||
dbpath=$db_path
|
dbpath=$db_path
|
||||||
query = SELECT destination from (SELECT destination, 0 as priority FROM aliases WHERE source='%s' UNION SELECT email as destination, 1 as priority FROM users WHERE email='%s') ORDER BY priority LIMIT 1;
|
query = SELECT permitted_senders FROM (SELECT permitted_senders, 0 AS priority FROM aliases WHERE source='%s' AND permitted_senders IS NOT NULL UNION SELECT destination AS permitted_senders, 1 AS priority FROM aliases WHERE source='%s' AND permitted_senders IS NULL UNION SELECT email as permitted_senders, 2 AS priority FROM users WHERE email='%s') ORDER BY priority LIMIT 1;
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# ### Destination Validation
|
# ### Destination Validation
|
||||||
@ -95,13 +100,13 @@ tools/editconf.py /etc/postfix/main.cf \
|
|||||||
virtual_alias_maps=sqlite:/etc/postfix/virtual-alias-maps.cf \
|
virtual_alias_maps=sqlite:/etc/postfix/virtual-alias-maps.cf \
|
||||||
local_recipient_maps=\$virtual_mailbox_maps
|
local_recipient_maps=\$virtual_mailbox_maps
|
||||||
|
|
||||||
# SQL statement to check if we handle mail for a domain, either for users or aliases.
|
# SQL statement to check if we handle incoming mail for a domain, either for users or aliases.
|
||||||
cat > /etc/postfix/virtual-mailbox-domains.cf << EOF;
|
cat > /etc/postfix/virtual-mailbox-domains.cf << EOF;
|
||||||
dbpath=$db_path
|
dbpath=$db_path
|
||||||
query = SELECT 1 FROM users WHERE email LIKE '%%@%s' UNION SELECT 1 FROM aliases WHERE source LIKE '%%@%s'
|
query = SELECT 1 FROM users WHERE email LIKE '%%@%s' UNION SELECT 1 FROM aliases WHERE source LIKE '%%@%s'
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# SQL statement to check if we handle mail for a user.
|
# SQL statement to check if we handle incoming mail for a user.
|
||||||
cat > /etc/postfix/virtual-mailbox-maps.cf << EOF;
|
cat > /etc/postfix/virtual-mailbox-maps.cf << EOF;
|
||||||
dbpath=$db_path
|
dbpath=$db_path
|
||||||
query = SELECT 1 FROM users WHERE email='%s'
|
query = SELECT 1 FROM users WHERE email='%s'
|
||||||
@ -127,9 +132,13 @@ EOF
|
|||||||
# might be returned by the UNION, so the whole query is wrapped in
|
# might be returned by the UNION, so the whole query is wrapped in
|
||||||
# another select that prioritizes the alias definition to preserve
|
# another select that prioritizes the alias definition to preserve
|
||||||
# postfix's preference for aliases for whole email addresses.
|
# postfix's preference for aliases for whole email addresses.
|
||||||
|
#
|
||||||
|
# Since we might have alias records with an empty destination because
|
||||||
|
# it might have just permitted_senders, skip any records with an
|
||||||
|
# empty destination here so that other lower priority rules might match.
|
||||||
cat > /etc/postfix/virtual-alias-maps.cf << EOF;
|
cat > /etc/postfix/virtual-alias-maps.cf << EOF;
|
||||||
dbpath=$db_path
|
dbpath=$db_path
|
||||||
query = SELECT destination from (SELECT destination, 0 as priority FROM aliases WHERE source='%s' UNION SELECT email as destination, 1 as priority FROM users WHERE email='%s') ORDER BY priority LIMIT 1;
|
query = SELECT destination from (SELECT destination, 0 as priority FROM aliases WHERE source='%s' AND destination<>'' UNION SELECT email as destination, 1 as priority FROM users WHERE email='%s') ORDER BY priority LIMIT 1;
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Restart Services
|
# Restart Services
|
||||||
|
@ -101,6 +101,16 @@ def migration_8(env):
|
|||||||
# a new key, which will be 2048 bits.
|
# a new key, which will be 2048 bits.
|
||||||
os.unlink(os.path.join(env['STORAGE_ROOT'], 'mail/dkim/mail.private'))
|
os.unlink(os.path.join(env['STORAGE_ROOT'], 'mail/dkim/mail.private'))
|
||||||
|
|
||||||
|
def migration_9(env):
|
||||||
|
# Add a column to the aliases table to store permitted_senders,
|
||||||
|
# which is a list of user account email addresses that are
|
||||||
|
# permitted to send mail using this alias instead of their own
|
||||||
|
# address. This was motivated by the addition of #427 ("Reject
|
||||||
|
# outgoing mail if FROM does not match Login") - which introduced
|
||||||
|
# the notion of outbound permitted-senders.
|
||||||
|
db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite')
|
||||||
|
shell("check_call", ["sqlite3", db, "ALTER TABLE aliases ADD permitted_senders TEXT"])
|
||||||
|
|
||||||
def get_current_migration():
|
def get_current_migration():
|
||||||
ver = 0
|
ver = 0
|
||||||
while True:
|
while True:
|
||||||
|
@ -120,10 +120,10 @@ elif sys.argv[1] == "alias" and len(sys.argv) == 2:
|
|||||||
print(mgmt("/mail/aliases"))
|
print(mgmt("/mail/aliases"))
|
||||||
|
|
||||||
elif sys.argv[1] == "alias" and sys.argv[2] == "add" and len(sys.argv) == 5:
|
elif sys.argv[1] == "alias" and sys.argv[2] == "add" and len(sys.argv) == 5:
|
||||||
print(mgmt("/mail/aliases/add", { "source": sys.argv[3], "destination": sys.argv[4] }))
|
print(mgmt("/mail/aliases/add", { "address": sys.argv[3], "forwards_to": sys.argv[4] }))
|
||||||
|
|
||||||
elif sys.argv[1] == "alias" and sys.argv[2] == "remove" and len(sys.argv) == 4:
|
elif sys.argv[1] == "alias" and sys.argv[2] == "remove" and len(sys.argv) == 4:
|
||||||
print(mgmt("/mail/aliases/remove", { "source": sys.argv[3] }))
|
print(mgmt("/mail/aliases/remove", { "address": sys.argv[3] }))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print("Invalid command-line arguments.")
|
print("Invalid command-line arguments.")
|
||||||
|
Loading…
Reference in New Issue
Block a user