1
0
mirror of https://github.com/mail-in-a-box/mailinabox.git synced 2025-01-23 12:37:05 +00:00

feat: renamed PRIMARY_HOSTNAME to BOX_HOSTNAME

using "primary" to describe the domain of the box / mail server is confusing when working with multiple domains.
Usually the box domain is different from the domain you want to host your mail for.
This commit is contained in:
tognee 2024-12-24 15:36:34 +01:00
parent 230a6dc7bf
commit 93d1055869
33 changed files with 235 additions and 228 deletions

4
Vagrantfile vendored
View File

@ -8,7 +8,7 @@ Vagrant.configure("2") do |config|
# to the public web. However, we currently don't want to expose SSH since # to the public web. However, we currently don't want to expose SSH since
# the machine's box will let anyone log into it. So instead we'll put the # the machine's box will let anyone log into it. So instead we'll put the
# machine on a private network. # machine on a private network.
config.vm.hostname = "mailinabox.lan" config.vm.hostname = "box.mailinabox.lan"
config.vm.network "private_network", ip: "192.168.56.4" config.vm.network "private_network", ip: "192.168.56.4"
config.vm.provision :shell, :inline => <<-SH config.vm.provision :shell, :inline => <<-SH
@ -18,7 +18,7 @@ Vagrant.configure("2") do |config|
export NONINTERACTIVE=1 export NONINTERACTIVE=1
export PUBLIC_IP=auto export PUBLIC_IP=auto
export PUBLIC_IPV6=auto export PUBLIC_IPV6=auto
export PRIMARY_HOSTNAME=auto export BOX_HOSTNAME=auto
#export SKIP_NETWORK_CHECKS=1 #export SKIP_NETWORK_CHECKS=1
# Start the setup script. # Start the setup script.

View File

@ -13,19 +13,19 @@
<array> <array>
<dict> <dict>
<key>CalDAVAccountDescription</key> <key>CalDAVAccountDescription</key>
<string>PRIMARY_HOSTNAME calendar</string> <string>BOX_HOSTNAME calendar</string>
<key>CalDAVHostName</key> <key>CalDAVHostName</key>
<string>PRIMARY_HOSTNAME</string> <string>BOX_HOSTNAME</string>
<key>CalDAVPort</key> <key>CalDAVPort</key>
<real>443</real> <real>443</real>
<key>CalDAVUseSSL</key> <key>CalDAVUseSSL</key>
<true/> <true/>
<key>PayloadDescription</key> <key>PayloadDescription</key>
<string>PRIMARY_HOSTNAME (Mail-in-a-Box)</string> <string>BOX_HOSTNAME (Mail-in-a-Box)</string>
<key>PayloadDisplayName</key> <key>PayloadDisplayName</key>
<string>PRIMARY_HOSTNAME calendar</string> <string>BOX_HOSTNAME calendar</string>
<key>PayloadIdentifier</key> <key>PayloadIdentifier</key>
<string>email.mailinabox.mobileconfig.PRIMARY_HOSTNAME.CalDAV</string> <string>email.mailinabox.mobileconfig.BOX_HOSTNAME.CalDAV</string>
<key>PayloadOrganization</key> <key>PayloadOrganization</key>
<string></string> <string></string>
<key>PayloadType</key> <key>PayloadType</key>
@ -37,13 +37,13 @@
</dict> </dict>
<dict> <dict>
<key>EmailAccountDescription</key> <key>EmailAccountDescription</key>
<string>PRIMARY_HOSTNAME mail</string> <string>BOX_HOSTNAME mail</string>
<key>EmailAccountType</key> <key>EmailAccountType</key>
<string>EmailTypeIMAP</string> <string>EmailTypeIMAP</string>
<key>IncomingMailServerAuthentication</key> <key>IncomingMailServerAuthentication</key>
<string>EmailAuthPassword</string> <string>EmailAuthPassword</string>
<key>IncomingMailServerHostName</key> <key>IncomingMailServerHostName</key>
<string>PRIMARY_HOSTNAME</string> <string>BOX_HOSTNAME</string>
<key>IncomingMailServerPortNumber</key> <key>IncomingMailServerPortNumber</key>
<integer>993</integer> <integer>993</integer>
<key>IncomingMailServerUseSSL</key> <key>IncomingMailServerUseSSL</key>
@ -51,7 +51,7 @@
<key>OutgoingMailServerAuthentication</key> <key>OutgoingMailServerAuthentication</key>
<string>EmailAuthPassword</string> <string>EmailAuthPassword</string>
<key>OutgoingMailServerHostName</key> <key>OutgoingMailServerHostName</key>
<string>PRIMARY_HOSTNAME</string> <string>BOX_HOSTNAME</string>
<key>OutgoingMailServerPortNumber</key> <key>OutgoingMailServerPortNumber</key>
<integer>465</integer> <integer>465</integer>
<key>OutgoingMailServerUseSSL</key> <key>OutgoingMailServerUseSSL</key>
@ -59,11 +59,11 @@
<key>OutgoingPasswordSameAsIncomingPassword</key> <key>OutgoingPasswordSameAsIncomingPassword</key>
<true/> <true/>
<key>PayloadDescription</key> <key>PayloadDescription</key>
<string>PRIMARY_HOSTNAME (Mail-in-a-Box)</string> <string>BOX_HOSTNAME (Mail-in-a-Box)</string>
<key>PayloadDisplayName</key> <key>PayloadDisplayName</key>
<string>PRIMARY_HOSTNAME mail</string> <string>BOX_HOSTNAME mail</string>
<key>PayloadIdentifier</key> <key>PayloadIdentifier</key>
<string>email.mailinabox.mobileconfig.PRIMARY_HOSTNAME.E-Mail</string> <string>email.mailinabox.mobileconfig.BOX_HOSTNAME.E-Mail</string>
<key>PayloadOrganization</key> <key>PayloadOrganization</key>
<string></string> <string></string>
<key>PayloadType</key> <key>PayloadType</key>
@ -81,9 +81,9 @@
</dict> </dict>
<dict> <dict>
<key>CardDAVAccountDescription</key> <key>CardDAVAccountDescription</key>
<string>PRIMARY_HOSTNAME contacts</string> <string>BOX_HOSTNAME contacts</string>
<key>CardDAVHostName</key> <key>CardDAVHostName</key>
<string>PRIMARY_HOSTNAME</string> <string>BOX_HOSTNAME</string>
<key>CardDAVPort</key> <key>CardDAVPort</key>
<integer>443</integer> <integer>443</integer>
<key>CardDAVPrincipalURL</key> <key>CardDAVPrincipalURL</key>
@ -91,11 +91,11 @@
<key>CardDAVUseSSL</key> <key>CardDAVUseSSL</key>
<true/> <true/>
<key>PayloadDescription</key> <key>PayloadDescription</key>
<string>PRIMARY_HOSTNAME (Mail-in-a-Box)</string> <string>BOX_HOSTNAME (Mail-in-a-Box)</string>
<key>PayloadDisplayName</key> <key>PayloadDisplayName</key>
<string>PRIMARY_HOSTNAME contacts</string> <string>BOX_HOSTNAME contacts</string>
<key>PayloadIdentifier</key> <key>PayloadIdentifier</key>
<string>email.mailinabox.mobileconfig.PRIMARY_HOSTNAME.carddav</string> <string>email.mailinabox.mobileconfig.BOX_HOSTNAME.carddav</string>
<key>PayloadOrganization</key> <key>PayloadOrganization</key>
<string></string> <string></string>
<key>PayloadType</key> <key>PayloadType</key>
@ -107,11 +107,11 @@
</dict> </dict>
</array> </array>
<key>PayloadDescription</key> <key>PayloadDescription</key>
<string>PRIMARY_HOSTNAME (Mail-in-a-Box)</string> <string>BOX_HOSTNAME (Mail-in-a-Box)</string>
<key>PayloadDisplayName</key> <key>PayloadDisplayName</key>
<string>PRIMARY_HOSTNAME</string> <string>BOX_HOSTNAME</string>
<key>PayloadIdentifier</key> <key>PayloadIdentifier</key>
<string>email.mailinabox.mobileconfig.PRIMARY_HOSTNAME</string> <string>email.mailinabox.mobileconfig.BOX_HOSTNAME</string>
<key>PayloadOrganization</key> <key>PayloadOrganization</key>
<string></string> <string></string>
<key>PayloadRemovalDisallowed</key> <key>PayloadRemovalDisallowed</key>

View File

@ -1,13 +1,13 @@
<?xml version="1.0"?> <?xml version="1.0"?>
<clientConfig version="1.1"> <clientConfig version="1.1">
<emailProvider id="PRIMARY_HOSTNAME"> <emailProvider id="BOX_HOSTNAME">
<domain>PRIMARY_HOSTNAME</domain> <domain>BOX_HOSTNAME</domain>
<displayName>PRIMARY_HOSTNAME (Mail-in-a-Box)</displayName> <displayName>BOX_HOSTNAME (Mail-in-a-Box)</displayName>
<displayShortName>PRIMARY_HOSTNAME</displayShortName> <displayShortName>BOX_HOSTNAME</displayShortName>
<incomingServer type="imap"> <incomingServer type="imap">
<hostname>PRIMARY_HOSTNAME</hostname> <hostname>BOX_HOSTNAME</hostname>
<port>993</port> <port>993</port>
<socketType>SSL</socketType> <socketType>SSL</socketType>
<username>%EMAILADDRESS%</username> <username>%EMAILADDRESS%</username>
@ -15,7 +15,7 @@
</incomingServer> </incomingServer>
<outgoingServer type="smtp"> <outgoingServer type="smtp">
<hostname>PRIMARY_HOSTNAME</hostname> <hostname>BOX_HOSTNAME</hostname>
<port>465</port> <port>465</port>
<socketType>SSL</socketType> <socketType>SSL</socketType>
<username>%EMAILADDRESS%</username> <username>%EMAILADDRESS%</username>
@ -24,14 +24,14 @@
<useGlobalPreferredServer>false</useGlobalPreferredServer> <useGlobalPreferredServer>false</useGlobalPreferredServer>
</outgoingServer> </outgoingServer>
<documentation url="https://PRIMARY_HOSTNAME/"> <documentation url="https://BOX_HOSTNAME/">
<descr lang="en">PRIMARY_HOSTNAME website.</descr> <descr lang="en">BOX_HOSTNAME website.</descr>
</documentation> </documentation>
</emailProvider> </emailProvider>
<webMail> <webMail>
<loginPage url="https://PRIMARY_HOSTNAME/mail/" /> <loginPage url="https://BOX_HOSTNAME/mail/" />
<loginPageInfo url="https://PRIMARY_HOSTNAME/mail/" > <loginPageInfo url="https://BOX_HOSTNAME/mail/" >
<username>%EMAILADDRESS%</username> <username>%EMAILADDRESS%</username>
<usernameField id="rcmloginuser" name="_user" /> <usernameField id="rcmloginuser" name="_user" />
<passwordField id="rcmloginpwd" name="_pass" /> <passwordField id="rcmloginpwd" name="_pass" />
@ -39,6 +39,6 @@
</loginPageInfo> </loginPageInfo>
</webMail> </webMail>
<clientConfigUpdate url="https://PRIMARY_HOSTNAME/.well-known/autoconfig/mail/config-v1.1.xml" /> <clientConfigUpdate url="https://BOX_HOSTNAME/.well-known/autoconfig/mail/config-v1.1.xml" />
</clientConfig> </clientConfig>

View File

@ -1,4 +1,4 @@
version: STSv1 version: STSv1
mode: MODE mode: MODE
mx: PRIMARY_HOSTNAME mx: BOX_HOSTNAME
max_age: 604800 max_age: 604800

View File

@ -1,7 +1,7 @@
# Remove the first line of the Received: header. Note that we cannot fully remove the Received: header # Remove the first line of the Received: header. Note that we cannot fully remove the Received: header
# because OpenDKIM requires that a header be present when signing outbound mail. The first line is # because OpenDKIM requires that a header be present when signing outbound mail. The first line is
# where the user's home IP address would be. # where the user's home IP address would be.
/^\s*Received:[^\n]*(.*)/ REPLACE Received: from authenticated-user (PRIMARY_HOSTNAME [PUBLIC_IP])$1 /^\s*Received:[^\n]*(.*)/ REPLACE Received: from authenticated-user (BOX_HOSTNAME [PUBLIC_IP])$1
# Remove other typically private information. # Remove other typically private information.
/^\s*User-Agent:/ IGNORE /^\s*User-Agent:/ IGNORE

View File

@ -10,7 +10,7 @@ define('TIMEZONE', '');
// Defines the base path on the server // Defines the base path on the server
define('BASE_PATH', dirname($_SERVER['SCRIPT_FILENAME']). '/'); define('BASE_PATH', dirname($_SERVER['SCRIPT_FILENAME']). '/');
define('ZPUSH_HOST', 'PRIMARY_HOSTNAME'); define('ZPUSH_HOST', 'BOX_HOSTNAME');
define('USE_FULLEMAIL_FOR_LOGIN', true); define('USE_FULLEMAIL_FOR_LOGIN', true);

View File

@ -125,7 +125,7 @@ def index():
return render_template('index.html', return render_template('index.html',
hostname=env['PRIMARY_HOSTNAME'], hostname=env['BOX_HOSTNAME'],
storage_root=env['STORAGE_ROOT'], storage_root=env['STORAGE_ROOT'],
no_users_exist=no_users_exist, no_users_exist=no_users_exist,

View File

@ -22,13 +22,13 @@ DOMAIN_RE = r"^(?!\-)(?:[*][.])?(?:[a-zA-Z\d\-_]{0,62}[a-zA-Z\d_]\.){1,126}(?!\d
def get_dns_domains(env): def get_dns_domains(env):
# Add all domain names in use by email users and mail aliases, any # Add all domain names in use by email users and mail aliases, any
# domains we serve web for (except www redirects because that would # domains we serve web for (except www redirects because that would
# lead to infinite recursion here) and ensure PRIMARY_HOSTNAME is in the list. # lead to infinite recursion here) and ensure BOX_HOSTNAME is in the list.
from mailconfig import get_mail_domains from mailconfig import get_mail_domains
from web_update import get_web_domains from web_update import get_web_domains
domains = set() domains = set()
domains |= set(get_mail_domains(env)) domains |= set(get_mail_domains(env))
domains |= set(get_web_domains(env, include_www_redirects=False)) domains |= set(get_web_domains(env, include_www_redirects=False))
domains.add(env['PRIMARY_HOSTNAME']) domains.add(env['BOX_HOSTNAME'])
return domains return domains
def get_dns_zones(env): def get_dns_zones(env):
@ -144,10 +144,10 @@ def build_zones(env):
auto_domains = web_domains - set(get_web_domains(env, include_auto=False)) auto_domains = web_domains - set(get_web_domains(env, include_auto=False))
domains |= auto_domains # www redirects not included in the initial list, see above domains |= auto_domains # www redirects not included in the initial list, see above
# Add ns1/ns2+PRIMARY_HOSTNAME which must also have A/AAAA records # Add ns1/ns2+BOX_HOSTNAME which must also have A/AAAA records
# when the box is acting as authoritative DNS server for its domains. # when the box is acting as authoritative DNS server for its domains.
for ns in ("ns1", "ns2"): for ns in ("ns1", "ns2"):
d = ns + "." + env["PRIMARY_HOSTNAME"] d = ns + "." + env["BOX_HOSTNAME"]
domains.add(d) domains.add(d)
auto_domains.add(d) auto_domains.add(d)
@ -161,9 +161,9 @@ def build_zones(env):
for domain in domains for domain in domains
} }
# For MTA-STS, we'll need to check if the PRIMARY_HOSTNAME certificate is # For MTA-STS, we'll need to check if the BOX_HOSTNAME certificate is
# singned and valid. Check that now rather than repeatedly for each domain. # singned and valid. Check that now rather than repeatedly for each domain.
domains[env["PRIMARY_HOSTNAME"]]["certificate-is-valid"] = is_domain_cert_signed_and_valid(env["PRIMARY_HOSTNAME"], env) domains[env["BOX_HOSTNAME"]]["certificate-is-valid"] = is_domain_cert_signed_and_valid(env["BOX_HOSTNAME"], env)
# Load custom records to add to zones. # Load custom records to add to zones.
additional_records = list(get_custom_dns_config(env)) additional_records = list(get_custom_dns_config(env))
@ -186,19 +186,19 @@ def build_zone(domain, domain_properties, additional_records, env, is_zone=True)
# 'False' in the tuple indicates these records would not be used if the zone # 'False' in the tuple indicates these records would not be used if the zone
# is managed outside of the box. # is managed outside of the box.
if is_zone: if is_zone:
# Obligatory NS record to ns1.PRIMARY_HOSTNAME. # Obligatory NS record to ns1.BOX_HOSTNAME.
records.append((None, "NS", "ns1.%s." % env["PRIMARY_HOSTNAME"], False)) records.append((None, "NS", "ns1.%s." % env["BOX_HOSTNAME"], False))
# NS record to ns2.PRIMARY_HOSTNAME or whatever the user overrides. # NS record to ns2.BOX_HOSTNAME or whatever the user overrides.
# User may provide one or more additional nameservers # User may provide one or more additional nameservers
secondary_ns_list = get_secondary_dns(additional_records, mode="NS") \ secondary_ns_list = get_secondary_dns(additional_records, mode="NS") \
or ["ns2." + env["PRIMARY_HOSTNAME"]] or ["ns2." + env["BOX_HOSTNAME"]]
records.extend((None, "NS", secondary_ns+'.', False) for secondary_ns in secondary_ns_list) records.extend((None, "NS", secondary_ns+'.', False) for secondary_ns in secondary_ns_list)
# In PRIMARY_HOSTNAME... # In BOX_HOSTNAME...
if domain == env["PRIMARY_HOSTNAME"]: if domain == env["BOX_HOSTNAME"]:
# Set the A/AAAA records. Do this early for the PRIMARY_HOSTNAME so that the user cannot override them # Set the A/AAAA records. Do this early for the BOX_HOSTNAME so that the user cannot override them
# and we can provide different explanatory text. # and we can provide different explanatory text.
records.append((None, "A", env["PUBLIC_IP"], "Required. Sets the IP address of the box.")) records.append((None, "A", env["PUBLIC_IP"], "Required. Sets the IP address of the box."))
if env.get("PUBLIC_IPV6"): records.append((None, "AAAA", env["PUBLIC_IPV6"], "Required. Sets the IPv6 address of the box.")) if env.get("PUBLIC_IPV6"): records.append((None, "AAAA", env["PUBLIC_IPV6"], "Required. Sets the IPv6 address of the box."))
@ -281,7 +281,7 @@ def build_zone(domain, domain_properties, additional_records, env, is_zone=True)
if domain_properties[domain]["mail"]: if domain_properties[domain]["mail"]:
# The MX record says where email for the domain should be delivered: Here! # The MX record says where email for the domain should be delivered: Here!
if not has_rec(None, "MX", prefix="10 "): if not has_rec(None, "MX", prefix="10 "):
records.append((None, "MX", "10 %s." % env["PRIMARY_HOSTNAME"], "Required. Specifies the hostname (and priority) of the machine that handles @%s mail." % domain)) records.append((None, "MX", "10 %s." % env["BOX_HOSTNAME"], "Required. Specifies the hostname (and priority) of the machine that handles @%s mail." % domain))
# SPF record: Permit the box ('mx', see above) to send mail on behalf of # SPF record: Permit the box ('mx', see above) to send mail on behalf of
# the domain, and no one else. # the domain, and no one else.
@ -304,14 +304,14 @@ def build_zone(domain, domain_properties, additional_records, env, is_zone=True)
records.append(("_dmarc", "TXT", 'v=DMARC1; p=quarantine;', "Recommended. Specifies that mail that does not originate from the box but claims to be from @%s or which does not have a valid DKIM signature is suspect and should be quarantined by the recipient's mail system." % domain)) records.append(("_dmarc", "TXT", 'v=DMARC1; p=quarantine;', "Recommended. Specifies that mail that does not originate from the box but claims to be from @%s or which does not have a valid DKIM signature is suspect and should be quarantined by the recipient's mail system." % domain))
if domain_properties[domain]["user"]: if domain_properties[domain]["user"]:
# Add CardDAV/CalDAV SRV records on the non-primary hostname that points to the primary hostname # Add CardDAV/CalDAV SRV records on the non-box hostname that points to the box hostname
# for autoconfiguration of mail clients (so only domains hosting user accounts need it). # for autoconfiguration of mail clients (so only domains hosting user accounts need it).
# The SRV record format is priority (0, whatever), weight (0, whatever), port, service provider hostname (w/ trailing dot). # The SRV record format is priority (0, whatever), weight (0, whatever), port, service provider hostname (w/ trailing dot).
if domain != env["PRIMARY_HOSTNAME"]: if domain != env["BOX_HOSTNAME"]:
for dav in ("card", "cal"): for dav in ("card", "cal"):
qname = "_" + dav + "davs._tcp" qname = "_" + dav + "davs._tcp"
if not has_rec(qname, "SRV"): if not has_rec(qname, "SRV"):
records.append((qname, "SRV", "0 0 443 " + env["PRIMARY_HOSTNAME"] + ".", "Recommended. Specifies the hostname of the server that handles CardDAV/CalDAV services for email addresses on this domain.")) records.append((qname, "SRV", "0 0 443 " + env["BOX_HOSTNAME"] + ".", "Recommended. Specifies the hostname of the server that handles CardDAV/CalDAV services for email addresses on this domain."))
# If this is a domain name that there are email addresses configured for, i.e. "something@" # If this is a domain name that there are email addresses configured for, i.e. "something@"
# this domain name, then the domain name is a MTA-STS (https://tools.ietf.org/html/rfc8461) # this domain name, then the domain name is a MTA-STS (https://tools.ietf.org/html/rfc8461)
@ -324,7 +324,7 @@ def build_zone(domain, domain_properties, additional_records, env, is_zone=True)
# #
# The policy itself is served at the "mta-sts" (no underscore) subdomain over HTTPS. Therefore # The policy itself is served at the "mta-sts" (no underscore) subdomain over HTTPS. Therefore
# the TLS certificate used by Postfix for STARTTLS must be a valid certificate for the MX # the TLS certificate used by Postfix for STARTTLS must be a valid certificate for the MX
# domain name (PRIMARY_HOSTNAME) *and* the TLS certificate used by nginx for HTTPS on the mta-sts # domain name (BOX_HOSTNAME) *and* the TLS certificate used by nginx for HTTPS on the mta-sts
# subdomain must be valid certificate for that domain. Do not set an MTA-STS policy if either # subdomain must be valid certificate for that domain. Do not set an MTA-STS policy if either
# certificate in use is not valid (e.g. because it is self-signed and a valid certificate has not # certificate in use is not valid (e.g. because it is self-signed and a valid certificate has not
# yet been provisioned). Since we cannot provision a certificate without A/AAAA records, we # yet been provisioned). Since we cannot provision a certificate without A/AAAA records, we
@ -332,7 +332,7 @@ def build_zone(domain, domain_properties, additional_records, env, is_zone=True)
# being valid certificates. # being valid certificates.
mta_sts_records = [ ] mta_sts_records = [ ]
if domain_properties[domain]["mail"] \ if domain_properties[domain]["mail"] \
and domain_properties[env["PRIMARY_HOSTNAME"]]["certificate-is-valid"] \ and domain_properties[env["BOX_HOSTNAME"]]["certificate-is-valid"] \
and is_domain_cert_signed_and_valid("mta-sts." + domain, env): and is_domain_cert_signed_and_valid("mta-sts." + domain, env):
# Compute an up-to-32-character hash of the policy file. We'll take a SHA-1 hash of the policy # Compute an up-to-32-character hash of the policy file. We'll take a SHA-1 hash of the policy
# file (20 bytes) and encode it as base-64 (28 bytes, using alphanumeric alternate characters # file (20 bytes) and encode it as base-64 (28 bytes, using alphanumeric alternate characters
@ -479,7 +479,7 @@ def write_nsd_zone(domain, zonefile, records, env, force):
# ldns-signzone, however. It used to say '; default zone domain'. # ldns-signzone, however. It used to say '; default zone domain'.
# #
# The SOA contact address for all of the domains on this system is hostmaster # The SOA contact address for all of the domains on this system is hostmaster
# @ the PRIMARY_HOSTNAME. Hopefully that's legit. # @ the BOX_HOSTNAME. Hopefully that's legit.
# #
# For the refresh through TTL fields, a good reference is: # For the refresh through TTL fields, a good reference is:
# https://www.ripe.net/publications/docs/ripe-203 # https://www.ripe.net/publications/docs/ripe-203
@ -492,7 +492,7 @@ def write_nsd_zone(domain, zonefile, records, env, force):
$ORIGIN {domain}. $ORIGIN {domain}.
$TTL 86400 ; default time to live $TTL 86400 ; default time to live
@ IN SOA ns1.{primary_domain}. hostmaster.{primary_domain}. ( @ IN SOA ns1.{box_domain}. hostmaster.{box_domain}. (
__SERIAL__ ; serial number __SERIAL__ ; serial number
7200 ; Refresh (secondary nameserver update interval) 7200 ; Refresh (secondary nameserver update interval)
3600 ; Retry (when refresh fails, how often to try again, should be lower than the refresh) 3600 ; Retry (when refresh fails, how often to try again, should be lower than the refresh)
@ -502,7 +502,7 @@ $TTL 86400 ; default time to live
""" """
# Replace replacement strings. # Replace replacement strings.
zone = zone.format(domain=domain, primary_domain=env["PRIMARY_HOSTNAME"]) zone = zone.format(domain=domain, box_domain=env["BOX_HOSTNAME"])
# Add records. # Add records.
for subdomain, querytype, value, _explanation in records: for subdomain, querytype, value, _explanation in records:

View File

@ -22,7 +22,7 @@ env = load_environment()
subject = sys.argv[1] subject = sys.argv[1]
# Administrator's email address. # Administrator's email address.
admin_addr = "administrator@" + env['PRIMARY_HOSTNAME'] admin_addr = "administrator@" + env['BOX_HOSTNAME']
# Read in STDIN. # Read in STDIN.
content = sys.stdin.read().strip() content = sys.stdin.read().strip()
@ -37,9 +37,9 @@ msg = MIMEMultipart('alternative')
# In Python 3.6: # In Python 3.6:
#msg = Message() #msg = Message()
msg['From'] = '"{}" <{}>'.format(env['PRIMARY_HOSTNAME'], admin_addr) msg['From'] = '"{}" <{}>'.format(env['BOX_HOSTNAME'], admin_addr)
msg['To'] = admin_addr msg['To'] = admin_addr
msg['Subject'] = "[{}] {}".format(env['PRIMARY_HOSTNAME'], subject) msg['Subject'] = "[{}] {}".format(env['BOX_HOSTNAME'], subject)
content_html = f'<html><body><pre style="overflow-x: scroll; white-space: pre;">{html.escape(content)}</pre></body></html>' content_html = f'<html><body><pre style="overflow-x: scroll; white-space: pre;">{html.escape(content)}</pre></body></html>'

View File

@ -517,7 +517,7 @@ def add_auto_aliases(aliases, env):
conn.commit() conn.commit()
def get_system_administrator(env): def get_system_administrator(env):
return "administrator@" + env['PRIMARY_HOSTNAME'] return "administrator@" + env['BOX_HOSTNAME']
def get_required_aliases(env): def get_required_aliases(env):
# These are the aliases that must exist. # These are the aliases that must exist.
@ -527,7 +527,7 @@ def get_required_aliases(env):
aliases.add(get_system_administrator(env)) aliases.add(get_system_administrator(env))
# The hostmaster alias is exposed in the DNS SOA for each zone. # The hostmaster alias is exposed in the DNS SOA for each zone.
aliases.add("hostmaster@" + env['PRIMARY_HOSTNAME']) aliases.add("hostmaster@" + env['BOX_HOSTNAME'])
# Get a list of domains we serve mail for, except ones for which the only # Get a list of domains we serve mail for, except ones for which the only
# 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.

View File

@ -83,7 +83,7 @@ def provision_totp(email, env):
# Make a URI that we encode within a QR code. # Make a URI that we encode within a QR code.
uri = pyotp.TOTP(secret).provisioning_uri( uri = pyotp.TOTP(secret).provisioning_uri(
name=email, name=email,
issuer_name=env["PRIMARY_HOSTNAME"] + " Mail-in-a-Box Control Panel" issuer_name=env["BOX_HOSTNAME"] + " Mail-in-a-Box Control Panel"
) )
# Generate a QR code as a base64-encode PNG image. # Generate a QR code as a base64-encode PNG image.

View File

@ -28,7 +28,7 @@ def get_ssl_certificates(env):
if fn == 'ssl_certificate.pem': if fn == 'ssl_certificate.pem':
# This is always a symbolic link # This is always a symbolic link
# to the certificate to use for # to the certificate to use for
# PRIMARY_HOSTNAME. Don't let it # BOX_HOSTNAME. Don't let it
# be eligible for use because we # be eligible for use because we
# could end up creating a symlink # could end up creating a symlink
# to itself --- we want to find # to itself --- we want to find
@ -73,8 +73,8 @@ def get_ssl_certificates(env):
domains = { } domains = { }
for cert in certificates: for cert in certificates:
# What domains is this certificate good for? # What domains is this certificate good for?
cert_domains, primary_domain = get_certificate_domains(cert["cert"]) cert_domains, common_name = get_certificate_domains(cert["cert"])
cert["primary_domain"] = primary_domain cert["common_name"] = common_name
# Is there a private key file for this certificate? # Is there a private key file for this certificate?
private_key = private_keys.get(cert["cert"].public_key().public_numbers()) private_key = private_keys.get(cert["cert"].public_key().public_numbers())
@ -84,9 +84,9 @@ def get_ssl_certificates(env):
# Add this cert to the list of certs usable for the domains. # Add this cert to the list of certs usable for the domains.
for domain in cert_domains: for domain in cert_domains:
# The primary hostname can only use a certificate mapped # The box hostname can only use a certificate mapped
# to the system private key. # to the system private key.
if domain == env['PRIMARY_HOSTNAME'] and cert["private_key"]["filename"] != os.path.join(env['STORAGE_ROOT'], 'ssl', 'ssl_private_key.pem'): if domain == env['BOX_HOSTNAME'] and cert["private_key"]["filename"] != os.path.join(env['STORAGE_ROOT'], 'ssl', 'ssl_private_key.pem'):
continue continue
domains.setdefault(domain, []).append(cert) domains.setdefault(domain, []).append(cert)
@ -134,7 +134,7 @@ def get_ssl_certificates(env):
ret[domain] = { ret[domain] = {
"private-key": cert["private_key"]["filename"], "private-key": cert["private_key"]["filename"],
"certificate": cert["filename"], "certificate": cert["filename"],
"primary-domain": cert["primary_domain"], "common-name": cert["common_name"],
"certificate_object": cert["cert"], "certificate_object": cert["cert"],
} }
@ -148,12 +148,12 @@ def get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=False
system_certificate = { system_certificate = {
"private-key": ssl_private_key, "private-key": ssl_private_key,
"certificate": ssl_certificate, "certificate": ssl_certificate,
"primary-domain": env['PRIMARY_HOSTNAME'], "common-name": env['BOX_HOSTNAME'],
"certificate_object": load_pem(load_cert_chain(ssl_certificate)[0]), "certificate_object": load_pem(load_cert_chain(ssl_certificate)[0]),
} }
if use_main_cert and domain == env['PRIMARY_HOSTNAME']: if use_main_cert and domain == env['BOX_HOSTNAME']:
# The primary domain must use the server certificate because # The box domain must use the server certificate because
# it is hard-coded in some service configuration files. # it is hard-coded in some service configuration files.
return system_certificate return system_certificate
@ -263,7 +263,7 @@ def provision_certificates(env, limit_domains):
# we'll create a list of lists of domains where the inner lists have # we'll create a list of lists of domains where the inner lists have
# at most 100 items. By sorting we also get the DNS zone domain as the first # at most 100 items. By sorting we also get the DNS zone domain as the first
# entry in each list (unless we overflow beyond 100) which ends up as the # entry in each list (unless we overflow beyond 100) which ends up as the
# primary domain listed in each certificate. # first domain listed in each certificate.
from dns_update import get_dns_zones from dns_update import get_dns_zones
certs = { } certs = { }
for zone, _zonefile in get_dns_zones(env): for zone, _zonefile in get_dns_zones(env):
@ -467,21 +467,21 @@ def install_cert_copy_file(fn, env):
def post_install_func(env): def post_install_func(env):
ret = [] ret = []
# Get the certificate to use for PRIMARY_HOSTNAME. # Get the certificate to use for BOX_HOSTNAME.
ssl_certificates = get_ssl_certificates(env) ssl_certificates = get_ssl_certificates(env)
cert = get_domain_ssl_files(env['PRIMARY_HOSTNAME'], ssl_certificates, env, use_main_cert=False) cert = get_domain_ssl_files(env['BOX_HOSTNAME'], ssl_certificates, env, use_main_cert=False)
if not cert: if not cert:
# Ruh-row, we don't have any certificate usable # Ruh-row, we don't have any certificate usable
# for the primary hostname. # for the box hostname.
ret.append("there is no valid certificate for " + env['PRIMARY_HOSTNAME']) ret.append("there is no valid certificate for " + env['BOX_HOSTNAME'])
# Symlink the best cert for PRIMARY_HOSTNAME to the system # Symlink the best cert for BOX_HOSTNAME to the system
# certificate path, which is hard-coded for various purposes, and then # certificate path, which is hard-coded for various purposes, and then
# restart postfix and dovecot. # restart postfix and dovecot.
system_ssl_certificate = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_certificate.pem')) system_ssl_certificate = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_certificate.pem'))
if cert and os.readlink(system_ssl_certificate) != cert['certificate']: if cert and os.readlink(system_ssl_certificate) != cert['certificate']:
# Update symlink. # Update symlink.
ret.append("updating primary certificate") ret.append("updating box certificate")
ssl_certificate = cert['certificate'] ssl_certificate = cert['certificate']
os.unlink(system_ssl_certificate) os.unlink(system_ssl_certificate)
os.symlink(ssl_certificate, system_ssl_certificate) os.symlink(ssl_certificate, system_ssl_certificate)
@ -523,7 +523,7 @@ def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring
# First check that the domain name is one of the names allowed by # First check that the domain name is one of the names allowed by
# the certificate. # the certificate.
if domain is not None: if domain is not None:
certificate_names, _cert_primary_name = get_certificate_domains(cert) certificate_names, _cn = get_certificate_domains(cert)
# Check that the domain appears among the acceptable names, or a wildcard # Check that the domain appears among the acceptable names, or a wildcard
# form of the domain name (which is a stricter check than the specs but # form of the domain name (which is a stricter check than the specs but
@ -558,9 +558,9 @@ def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring
if cert.issuer == cert.subject: if cert.issuer == cert.subject:
return ("SELF-SIGNED", None) return ("SELF-SIGNED", None)
# When selecting which certificate to use for non-primary domains, we check if the primary # When selecting which certificate to use for non-registrable domains, we check if the
# certificate or a www-parent-domain certificate is good for the domain. There's no need # registrable domain certificate or a www-parent-domain certificate is good for the domain.
# to run extra checks beyond this point. # There's no need to run extra checks beyond this point.
if just_check_domain: if just_check_domain:
return ("OK", None) return ("OK", None)

View File

@ -216,7 +216,7 @@ def check_software_updates(env, output):
def check_system_aliases(env, output): def check_system_aliases(env, output):
# Check that the administrator alias exists since that's where all # Check that the administrator alias exists since that's where all
# admin email is automatically directed. # admin email is automatically directed.
check_alias_exists("System administrator address", "administrator@" + env['PRIMARY_HOSTNAME'], env, output) check_alias_exists("System administrator address", "administrator@" + env['BOX_HOSTNAME'], env, output)
def check_free_disk_space(rounded_values, env, output): def check_free_disk_space(rounded_values, env, output):
# Check free disk space. # Check free disk space.
@ -382,8 +382,8 @@ def run_domain_checks_on_domain(domain, rounded_time, env, dns_domains, dns_zone
output.add_heading(domain) output.add_heading(domain)
output.print_error("Domain name is invalid: " + str(e)) output.print_error("Domain name is invalid: " + str(e))
if domain == env["PRIMARY_HOSTNAME"]: if domain == env["BOX_HOSTNAME"]:
check_primary_hostname_dns(domain, env, output, dns_domains, dns_zonefiles) check_box_hostname_dns(domain, env, output, dns_domains, dns_zonefiles)
if domain in dns_domains: if domain in dns_domains:
check_dns_zone(domain, env, output, dns_zonefiles) check_dns_zone(domain, env, output, dns_zonefiles)
@ -419,13 +419,13 @@ def run_domain_checks_on_domain(domain, rounded_time, env, dns_domains, dns_zone
return (domain, output) return (domain, output)
def check_primary_hostname_dns(domain, env, output, dns_domains, dns_zonefiles): def check_box_hostname_dns(domain, env, output, dns_domains, dns_zonefiles):
# If a DS record is set on the zone containing this domain, check DNSSEC now. # If a DS record is set on the zone containing this domain, check DNSSEC now.
has_dnssec = False has_dnssec = False
for zone in dns_domains: for zone in dns_domains:
if (zone == domain or domain.endswith("." + zone)) and query_dns(zone, "DS", nxdomain=None) is not None: if (zone == domain or domain.endswith("." + zone)) and query_dns(zone, "DS", nxdomain=None) is not None:
has_dnssec = True has_dnssec = True
check_dnssec(zone, env, output, dns_zonefiles, is_checking_primary=True) check_dnssec(zone, env, output, dns_zonefiles, is_checking_box_domain=True)
ip = query_dns(domain, "A") ip = query_dns(domain, "A")
ns_ips = query_dns("ns1." + domain, "A") + '/' + query_dns("ns2." + domain, "A") ns_ips = query_dns("ns1." + domain, "A") + '/' + query_dns("ns2." + domain, "A")
@ -437,35 +437,35 @@ def check_primary_hostname_dns(domain, env, output, dns_domains, dns_zonefiles):
# the nameserver, are reporting the right info --- but if the glue is incorrect this # the nameserver, are reporting the right info --- but if the glue is incorrect this
# will probably fail. # will probably fail.
if ns_ips == env['PUBLIC_IP'] + '/' + env['PUBLIC_IP']: if ns_ips == env['PUBLIC_IP'] + '/' + env['PUBLIC_IP']:
output.print_ok("Nameserver glue records are correct at registrar. [ns1/ns2.{}{}]".format(env['PRIMARY_HOSTNAME'], env['PUBLIC_IP'])) output.print_ok("Nameserver glue records are correct at registrar. [ns1/ns2.{}{}]".format(env['BOX_HOSTNAME'], env['PUBLIC_IP']))
elif ip == env['PUBLIC_IP']: elif ip == env['PUBLIC_IP']:
# The NS records are not what we expect, but the domain resolves correctly, so # The NS records are not what we expect, but the domain resolves correctly, so
# the user may have set up external DNS. List this discrepancy as a warning. # the user may have set up external DNS. List this discrepancy as a warning.
output.print_warning("""Nameserver glue records (ns1.{} and ns2.{}) should be configured at your domain name output.print_warning("""Nameserver glue records (ns1.{} and ns2.{}) should be configured at your domain name
registrar as having the IP address of this box ({}). They currently report addresses of {}. If you have set up External DNS, this may be OK.""".format(env['PRIMARY_HOSTNAME'], env['PRIMARY_HOSTNAME'], env['PUBLIC_IP'], ns_ips)) registrar as having the IP address of this box ({}). They currently report addresses of {}. If you have set up External DNS, this may be OK.""".format(env['BOX_HOSTNAME'], env['BOX_HOSTNAME'], env['PUBLIC_IP'], ns_ips))
else: else:
output.print_error("""Nameserver glue records are incorrect. The ns1.{} and ns2.{} nameservers must be configured at your domain name output.print_error("""Nameserver glue records are incorrect. The ns1.{} and ns2.{} nameservers must be configured at your domain name
registrar as having the IP address {}. They currently report addresses of {}. It may take several hours for registrar as having the IP address {}. They currently report addresses of {}. It may take several hours for
public DNS to update after a change.""".format(env['PRIMARY_HOSTNAME'], env['PRIMARY_HOSTNAME'], env['PUBLIC_IP'], ns_ips)) public DNS to update after a change.""".format(env['BOX_HOSTNAME'], env['BOX_HOSTNAME'], env['PUBLIC_IP'], ns_ips))
# Check that PRIMARY_HOSTNAME resolves to PUBLIC_IP[V6] in public DNS. # Check that BOX_HOSTNAME resolves to PUBLIC_IP[V6] in public DNS.
ipv6 = query_dns(domain, "AAAA") if env.get("PUBLIC_IPV6") else None ipv6 = query_dns(domain, "AAAA") if env.get("PUBLIC_IPV6") else None
if ip == env['PUBLIC_IP'] and not (ipv6 and env['PUBLIC_IPV6'] and ipv6 != normalize_ip(env['PUBLIC_IPV6'])): if ip == env['PUBLIC_IP'] and not (ipv6 and env['PUBLIC_IPV6'] and ipv6 != normalize_ip(env['PUBLIC_IPV6'])):
output.print_ok("Domain resolves to box's IP address. [{}{}]".format(env['PRIMARY_HOSTNAME'], my_ips)) output.print_ok("Domain resolves to box's IP address. [{}{}]".format(env['BOX_HOSTNAME'], my_ips))
else: else:
output.print_error("""This domain must resolve to this box's IP address ({}) in public DNS but it currently resolves output.print_error("""This domain must resolve to this box's IP address ({}) in public DNS but it currently resolves
to {}. It may take several hours for public DNS to update after a change. This problem may result from other to {}. It may take several hours for public DNS to update after a change. This problem may result from other
issues listed above.""".format(my_ips, ip + ((" / " + ipv6) if ipv6 is not None else ""))) issues listed above.""".format(my_ips, ip + ((" / " + ipv6) if ipv6 is not None else "")))
# Check reverse DNS matches the PRIMARY_HOSTNAME. Note that it might not be # Check reverse DNS matches the BOX_HOSTNAME. Note that it might not be
# a DNS zone if it is a subdomain of another domain we have a zone for. # a DNS zone if it is a subdomain of another domain we have a zone for.
existing_rdns_v4 = query_dns(dns.reversename.from_address(env['PUBLIC_IP']), "PTR") existing_rdns_v4 = query_dns(dns.reversename.from_address(env['PUBLIC_IP']), "PTR")
existing_rdns_v6 = query_dns(dns.reversename.from_address(env['PUBLIC_IPV6']), "PTR") if env.get("PUBLIC_IPV6") else None existing_rdns_v6 = query_dns(dns.reversename.from_address(env['PUBLIC_IPV6']), "PTR") if env.get("PUBLIC_IPV6") else None
if existing_rdns_v4 == domain and existing_rdns_v6 in {None, domain}: if existing_rdns_v4 == domain and existing_rdns_v6 in {None, domain}:
output.print_ok("Reverse DNS is set correctly at ISP. [{}{}]".format(my_ips, env['PRIMARY_HOSTNAME'])) output.print_ok("Reverse DNS is set correctly at ISP. [{}{}]".format(my_ips, env['BOX_HOSTNAME']))
elif existing_rdns_v4 == existing_rdns_v6 or existing_rdns_v6 is None: elif existing_rdns_v4 == existing_rdns_v6 or existing_rdns_v6 is None:
output.print_error(f"""This box's reverse DNS is currently {existing_rdns_v4}, but it should be {domain}. Your ISP or cloud provider will have instructions output.print_error(f"""This box's reverse DNS is currently {existing_rdns_v4}, but it should be {domain}. Your ISP or cloud provider will have instructions
on setting up reverse DNS for this box.""" ) on setting up reverse DNS for this box.""" )
@ -518,10 +518,10 @@ def check_dns_zone(domain, env, output, dns_zonefiles):
custom_dns_records = list(get_custom_dns_config(env)) # generator => list so we can reuse it custom_dns_records = list(get_custom_dns_config(env)) # generator => list so we can reuse it
correct_ip = "; ".join(sorted(get_custom_dns_records(custom_dns_records, domain, "A"))) or env['PUBLIC_IP'] correct_ip = "; ".join(sorted(get_custom_dns_records(custom_dns_records, domain, "A"))) or env['PUBLIC_IP']
custom_secondary_ns = get_secondary_dns(custom_dns_records, mode="NS") custom_secondary_ns = get_secondary_dns(custom_dns_records, mode="NS")
secondary_ns = custom_secondary_ns or ["ns2." + env['PRIMARY_HOSTNAME']] secondary_ns = custom_secondary_ns or ["ns2." + env['BOX_HOSTNAME']]
existing_ns = query_dns(domain, "NS") existing_ns = query_dns(domain, "NS")
correct_ns = "; ".join(sorted(["ns1." + env["PRIMARY_HOSTNAME"], *secondary_ns])) correct_ns = "; ".join(sorted(["ns1." + env["BOX_HOSTNAME"], *secondary_ns]))
ip = query_dns(domain, "A") ip = query_dns(domain, "A")
probably_external_dns = False probably_external_dns = False
@ -595,7 +595,7 @@ def check_dns_zone_suggestions(domain, env, output, dns_zonefiles, domains_with_
check_dnssec(domain, env, output, dns_zonefiles) check_dnssec(domain, env, output, dns_zonefiles)
def check_dnssec(domain, env, output, dns_zonefiles, is_checking_primary=False): def check_dnssec(domain, env, output, dns_zonefiles, is_checking_box_domain=False):
# See if the domain has a DS record set at the registrar. The DS record must # See if the domain has a DS record set at the registrar. The DS record must
# match one of the keys that we've used to sign the zone. It may use one of # match one of the keys that we've used to sign the zone. It may use one of
# several hashing algorithms. We've pre-generated all possible valid DS # several hashing algorithms. We've pre-generated all possible valid DS
@ -661,7 +661,7 @@ def check_dnssec(domain, env, output, dns_zonefiles, is_checking_primary=False):
IMPORTANT: Do not delete existing DNSSEC 'DS' records for this domain until confirmation that the new DNSSEC 'DS' record IMPORTANT: Do not delete existing DNSSEC 'DS' records for this domain until confirmation that the new DNSSEC 'DS' record
for this domain is valid.""") for this domain is valid.""")
else: else:
if is_checking_primary: if is_checking_box_domain:
output.print_error("""The DNSSEC 'DS' record for %s is incorrect. See further details below.""" % domain) output.print_error("""The DNSSEC 'DS' record for %s is incorrect. See further details below.""" % domain)
return return
output.print_error("""This domain's DNSSEC DS record is incorrect. The chain of trust is broken between the public DNS system output.print_error("""This domain's DNSSEC DS record is incorrect. The chain of trust is broken between the public DNS system
@ -702,7 +702,7 @@ def check_dnssec(domain, env, output, dns_zonefiles, is_checking_primary=False):
def check_mail_domain(domain, env, output): def check_mail_domain(domain, env, output):
# Check the MX record. # Check the MX record.
recommended_mx = "10 " + env['PRIMARY_HOSTNAME'] recommended_mx = "10 " + env['BOX_HOSTNAME']
mx = query_dns(domain, "MX", nxdomain=None) mx = query_dns(domain, "MX", nxdomain=None)
if mx is None or mx == "[timeout]": if mx is None or mx == "[timeout]":
@ -713,26 +713,26 @@ def check_mail_domain(domain, env, output):
mxhost = mx.split('; ')[0].split(' ')[1] mxhost = mx.split('; ')[0].split(' ')[1]
if mxhost is None: if mxhost is None:
# A missing MX record is okay on the primary hostname because # A missing MX record is okay on the box hostname because
# the primary hostname's A record (the MX fallback) is... itself, # the box hostname's A record (the MX fallback) is... itself,
# which is what we want the MX to be. # which is what we want the MX to be.
if domain == env['PRIMARY_HOSTNAME']: if domain == env['BOX_HOSTNAME']:
output.print_ok(f"Domain's email is directed to this domain. [{domain} has no MX record, which is ok]") output.print_ok(f"Domain's email is directed to this domain. [{domain} has no MX record, which is ok]")
# And a missing MX record is okay on other domains if the A record # And a missing MX record is okay on other domains if the A record
# matches the A record of the PRIMARY_HOSTNAME. Actually this will # matches the A record of the BOX_HOSTNAME. Actually this will
# probably confuse DANE TLSA, but we'll let that slide for now. # probably confuse DANE TLSA, but we'll let that slide for now.
else: else:
domain_a = query_dns(domain, "A", nxdomain=None) domain_a = query_dns(domain, "A", nxdomain=None)
primary_a = query_dns(env['PRIMARY_HOSTNAME'], "A", nxdomain=None) box_a = query_dns(env['BOX_HOSTNAME'], "A", nxdomain=None)
if domain_a is not None and domain_a == primary_a: if domain_a is not None and domain_a == box_a:
output.print_ok(f"Domain's email is directed to this domain. [{domain} has no MX record but its A record is OK]") output.print_ok(f"Domain's email is directed to this domain. [{domain} has no MX record but its A record is OK]")
else: else:
output.print_error(f"""This domain's DNS MX record is not set. It should be '{recommended_mx}'. Mail will not output.print_error(f"""This domain's DNS MX record is not set. It should be '{recommended_mx}'. Mail will not
be delivered to this box. It may take several hours for public DNS to update after a be delivered to this box. It may take several hours for public DNS to update after a
change. This problem may result from other issues listed here.""") change. This problem may result from other issues listed here.""")
elif mxhost == env['PRIMARY_HOSTNAME']: elif mxhost == env['BOX_HOSTNAME']:
good_news = f"Domain's email is directed to this domain. [{domain}{mx}]" good_news = f"Domain's email is directed to this domain. [{domain}{mx}]"
if mx != recommended_mx: if mx != recommended_mx:
good_news += f" This configuration is non-standard. The recommended configuration is '{recommended_mx}'." good_news += f" This configuration is non-standard. The recommended configuration is '{recommended_mx}'."
@ -743,7 +743,7 @@ def check_mail_domain(domain, env, output):
sts_resolver = postfix_mta_sts_resolver.resolver.STSResolver(loop=loop) sts_resolver = postfix_mta_sts_resolver.resolver.STSResolver(loop=loop)
valid, policy = loop.run_until_complete(sts_resolver.resolve(domain)) valid, policy = loop.run_until_complete(sts_resolver.resolve(domain))
if valid == postfix_mta_sts_resolver.resolver.STSFetchResult.VALID: if valid == postfix_mta_sts_resolver.resolver.STSFetchResult.VALID:
if policy[1].get("mx") == [env['PRIMARY_HOSTNAME']] and policy[1].get("mode") == "enforce": # policy[0] is the policyid if policy[1].get("mx") == [env['BOX_HOSTNAME']] and policy[1].get("mode") == "enforce": # policy[0] is the policyid
output.print_ok("MTA-STS policy is present.") output.print_ok("MTA-STS policy is present.")
else: else:
output.print_error(f"MTA-STS policy is present but has unexpected settings. [{policy[1]}]") output.print_error(f"MTA-STS policy is present but has unexpected settings. [{policy[1]}]")
@ -763,7 +763,7 @@ def check_mail_domain(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.
# The user might have chosen a domain that was previously in use by a spammer # The user might have chosen a domain that was previously in use by a spammer
# and will not be able to reliably send mail. # and will not be able to reliably send mail.
# See https://www.spamhaus.org/news/article/807/using-our-public-mirrors-check-your-return-codes-now. for # See https://www.spamhaus.org/news/article/807/using-our-public-mirrors-check-your-return-codes-now. for
# information on spamhaus return codes # information on spamhaus return codes
dbl = query_dns(domain+'.dbl.spamhaus.org', "A", nxdomain=None) dbl = query_dns(domain+'.dbl.spamhaus.org', "A", nxdomain=None)
@ -786,9 +786,9 @@ def check_mail_domain(domain, env, output):
def check_web_domain(domain, rounded_time, ssl_certificates, env, output): def check_web_domain(domain, rounded_time, ssl_certificates, env, output):
# See if the domain's A record resolves to our PUBLIC_IP. This is already checked # See if the domain's A record resolves to our PUBLIC_IP. This is already checked
# for PRIMARY_HOSTNAME, for which it is required for mail specifically. For it and # for BOX_HOSTNAME, for which it is required for mail specifically. For it and
# other domains, it is required to access its website. # other domains, it is required to access its website.
if domain != env['PRIMARY_HOSTNAME']: if domain != env['BOX_HOSTNAME']:
ok_values = [] ok_values = []
for (rtype, expected) in (("A", env['PUBLIC_IP']), ("AAAA", env.get('PUBLIC_IPV6'))): for (rtype, expected) in (("A", env['PUBLIC_IP']), ("AAAA", env.get('PUBLIC_IPV6'))):
if not expected: continue # IPv6 is not configured if not expected: continue # IPv6 is not configured
@ -805,7 +805,7 @@ def check_web_domain(domain, rounded_time, ssl_certificates, env, output):
output.print_ok("Domain resolves to this box's IP address. [{}{}]".format(domain, '; '.join(ok_values))) output.print_ok("Domain resolves to this box's IP address. [{}{}]".format(domain, '; '.join(ok_values)))
# We need a TLS certificate for PRIMARY_HOSTNAME because that's where the # We need a TLS certificate for BOX_HOSTNAME because that's where the
# user will log in with IMAP or webmail. Any other domain we serve a # user will log in with IMAP or webmail. Any other domain we serve a
# website for also needs a signed certificate. # website for also needs a signed certificate.
check_ssl_cert(domain, rounded_time, ssl_certificates, env, output) check_ssl_cert(domain, rounded_time, ssl_certificates, env, output)
@ -886,7 +886,7 @@ def check_ssl_cert(domain, rounded_time, ssl_certificates, env, output):
elif cert_status == "SELF-SIGNED": elif cert_status == "SELF-SIGNED":
# Offer instructions for purchasing a signed certificate. # Offer instructions for purchasing a signed certificate.
if domain == env['PRIMARY_HOSTNAME']: if domain == env['BOX_HOSTNAME']:
output.print_error("""The TLS (SSL) certificate for this domain is currently self-signed. You will get a security output.print_error("""The TLS (SSL) certificate for this domain is currently self-signed. You will get a security
warning when you check or send email and when visiting this domain in a web browser (for webmail or warning when you check or send email and when visiting this domain in a web browser (for webmail or
static site hosting).""") static site hosting).""")
@ -1140,9 +1140,9 @@ if __name__ == "__main__":
with multiprocessing.pool.Pool(processes=10) as pool: with multiprocessing.pool.Pool(processes=10) as pool:
run_and_output_changes(env, pool) run_and_output_changes(env, pool)
elif sys.argv[1] == "--check-primary-hostname": elif sys.argv[1] == "--check-box-hostname":
# See if the primary hostname appears resolvable and has a signed certificate. # See if the box hostname appears resolvable and has a signed certificate.
domain = env['PRIMARY_HOSTNAME'] domain = env['BOX_HOSTNAME']
if query_dns(domain, "A") != env['PUBLIC_IP']: if query_dns(domain, "A") != env['PUBLIC_IP']:
sys.exit(1) sys.exit(1)
ssl_certificates = get_ssl_certificates(env) ssl_certificates = get_ssl_certificates(env)

View File

@ -73,8 +73,8 @@ def sort_domains(domain_names, env):
# Sort the zones. # Sort the zones.
zone_domains = sorted(zones.values(), zone_domains = sorted(zones.values(),
key = lambda d : ( key = lambda d : (
# PRIMARY_HOSTNAME or the zone that contains it is always first. # BOX_HOSTNAME or the zone that contains it is always first.
not (d == env['PRIMARY_HOSTNAME'] or env['PRIMARY_HOSTNAME'].endswith("." + d)), not (d == env['BOX_HOSTNAME'] or env['BOX_HOSTNAME'].endswith("." + d)),
# Then just dumb lexicographically. # Then just dumb lexicographically.
d, d,
@ -86,11 +86,11 @@ def sort_domains(domain_names, env):
# First by zone. # First by zone.
zone_domains.index(zones[d]), zone_domains.index(zones[d]),
# PRIMARY_HOSTNAME is always first within the zone that contains it. # BOX_HOSTNAME is always first within the zone that contains it.
d != env['PRIMARY_HOSTNAME'], d != env['BOX_HOSTNAME'],
# Followed by any of its subdomains. # Followed by any of its subdomains.
not d.endswith("." + env['PRIMARY_HOSTNAME']), not d.endswith("." + env['BOX_HOSTNAME']),
# Then in right-to-left lexicographic order of the .-separated parts of the name. # Then in right-to-left lexicographic order of the .-separated parts of the name.
list(reversed(d.split("."))), list(reversed(d.split("."))),

View File

@ -39,10 +39,10 @@ def get_web_domains(env, include_www_redirects=True, include_auto=True, exclude_
# IP address than this box. Remove those domains from our list. # IP address than this box. Remove those domains from our list.
domains -= get_domains_with_a_records(env) domains -= get_domains_with_a_records(env)
# Ensure the PRIMARY_HOSTNAME is in the list so we can serve webmail # Ensure the BOX_HOSTNAME is in the list so we can serve webmail
# as well as Z-Push for Exchange ActiveSync. This can't be removed # as well as Z-Push for Exchange ActiveSync. This can't be removed
# by a custom A/AAAA record and is never a 'www.' redirect. # by a custom A/AAAA record and is never a 'www.' redirect.
domains.add(env['PRIMARY_HOSTNAME']) domains.add(env['BOX_HOSTNAME'])
# Sort the list so the nginx conf gets written in a stable order. # Sort the list so the nginx conf gets written in a stable order.
return sort_domains(domains, env) return sort_domains(domains, env)
@ -86,18 +86,18 @@ def do_web_update(env):
# Load the templates. # Load the templates.
template0 = read_conf("nginx.conf") template0 = read_conf("nginx.conf")
template1 = read_conf("nginx-alldomains.conf") template1 = read_conf("nginx-alldomains.conf")
template2 = read_conf("nginx-primaryonly.conf") template2 = read_conf("nginx-boxonly.conf")
template3 = "\trewrite ^(.*) https://$REDIRECT_DOMAIN$1 permanent;\n" template3 = "\trewrite ^(.*) https://$REDIRECT_DOMAIN$1 permanent;\n"
# Add the PRIMARY_HOST configuration first so it becomes nginx's default server. # Add the BOX_HOSTNAME configuration first so it becomes nginx's default server.
nginx_conf += make_domain_config(env['PRIMARY_HOSTNAME'], [template0, template1, template2], ssl_certificates, env) nginx_conf += make_domain_config(env['BOX_HOSTNAME'], [template0, template1, template2], ssl_certificates, env)
# Add configuration all other web domains. # Add configuration all other web domains.
has_root_proxy_or_redirect = get_web_domains_with_root_overrides(env) has_root_proxy_or_redirect = get_web_domains_with_root_overrides(env)
web_domains_not_redirect = get_web_domains(env, include_www_redirects=False) web_domains_not_redirect = get_web_domains(env, include_www_redirects=False)
for domain in get_web_domains(env): for domain in get_web_domains(env):
if domain == env['PRIMARY_HOSTNAME']: if domain == env['BOX_HOSTNAME']:
# PRIMARY_HOSTNAME is handled above. # BOX_HOSTNAME is handled above.
continue continue
if domain in web_domains_not_redirect: if domain in web_domains_not_redirect:
# This is a regular domain. # This is a regular domain.
@ -250,7 +250,7 @@ def get_web_domains_info(env):
def check_cert(domain): def check_cert(domain):
try: try:
tls_cert = get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=True) tls_cert = get_domain_ssl_files(domain, ssl_certificates, env, allow_missing_cert=True)
except OSError: # PRIMARY_HOSTNAME cert is missing except OSError: # BOX_HOSTNAME cert is missing
tls_cert = None tls_cert = None
if tls_cert is None: return ("danger", "No certificate installed.") if tls_cert is None: return ("danger", "No certificate installed.")
cert_status, cert_status_details = check_certificate(domain, tls_cert["certificate"], tls_cert["private-key"]) cert_status, cert_status_details = check_certificate(domain, tls_cert["certificate"], tls_cert["private-key"])

View File

@ -34,8 +34,8 @@ if [ -z "$(management/cli.py user)" ]; then
# But in a non-interactive shell, just make something up. # But in a non-interactive shell, just make something up.
# This is normally for testing. # This is normally for testing.
else else
# Use me@PRIMARY_HOSTNAME # Use me@BOX_HOSTNAME
EMAIL_ADDR=me@$PRIMARY_HOSTNAME EMAIL_ADDR=me@$BOX_HOSTNAME
EMAIL_PW=12345678 EMAIL_PW=12345678
echo echo
echo "Creating a new administrative mail account for $EMAIL_ADDR with password $EMAIL_PW." echo "Creating a new administrative mail account for $EMAIL_ADDR with password $EMAIL_PW."
@ -54,5 +54,5 @@ if [ -z "$(management/cli.py user)" ]; then
hide_output management/cli.py user make-admin "$EMAIL_ADDR" hide_output management/cli.py user make-admin "$EMAIL_ADDR"
# Create an alias to which we'll direct all automatically-created administrative aliases. # Create an alias to which we'll direct all automatically-created administrative aliases.
management/cli.py alias add "administrator@$PRIMARY_HOSTNAME" "$EMAIL_ADDR" > /dev/null management/cli.py alias add "administrator@$BOX_HOSTNAME" "$EMAIL_ADDR" > /dev/null
fi fi

View File

@ -152,7 +152,7 @@ EOF
# Setting a `postmaster_address` is required or LMTP won't start. An alias # Setting a `postmaster_address` is required or LMTP won't start. An alias
# will be created automatically by our management daemon. # will be created automatically by our management daemon.
tools/editconf.py /etc/dovecot/conf.d/15-lda.conf \ tools/editconf.py /etc/dovecot/conf.d/15-lda.conf \
"postmaster_address=postmaster@$PRIMARY_HOSTNAME" "postmaster_address=postmaster@$BOX_HOSTNAME"
# ### Sieve # ### Sieve

View File

@ -57,7 +57,7 @@ tools/editconf.py /etc/postfix/main.cf \
inet_interfaces=all \ inet_interfaces=all \
smtp_bind_address="$PRIVATE_IP" \ smtp_bind_address="$PRIVATE_IP" \
smtp_bind_address6="$PRIVATE_IPV6" \ smtp_bind_address6="$PRIVATE_IPV6" \
myhostname="$PRIMARY_HOSTNAME"\ myhostname="$BOX_HOSTNAME"\
smtpd_banner="\$myhostname ESMTP Hi, I'm a Mail-in-a-Box (Ubuntu/Postfix; see https://mailinabox.email/)" \ smtpd_banner="\$myhostname ESMTP Hi, I'm a Mail-in-a-Box (Ubuntu/Postfix; see https://mailinabox.email/)" \
mydestination=localhost mydestination=localhost
@ -121,7 +121,7 @@ cp conf/postfix_outgoing_mail_header_filters /etc/postfix/outgoing_mail_header_f
# Modify the `outgoing_mail_header_filters` file to use the local machine name and ip # Modify the `outgoing_mail_header_filters` file to use the local machine name and ip
# on the first received header line. This may help reduce the spam score of email by # on the first received header line. This may help reduce the spam score of email by
# removing the 127.0.0.1 reference. # removing the 127.0.0.1 reference.
sed -i "s/PRIMARY_HOSTNAME/$PRIMARY_HOSTNAME/" /etc/postfix/outgoing_mail_header_filters sed -i "s/BOX_HOSTNAME/$BOX_HOSTNAME/" /etc/postfix/outgoing_mail_header_filters
sed -i "s/PUBLIC_IP/$PUBLIC_IP/" /etc/postfix/outgoing_mail_header_filters sed -i "s/PUBLIC_IP/$PUBLIC_IP/" /etc/postfix/outgoing_mail_header_filters
# Enable TLS on incoming connections. It is not required on port 25, allowing for opportunistic # Enable TLS on incoming connections. It is not required on port 25, allowing for opportunistic

View File

@ -149,36 +149,36 @@ def migration_11(env):
def migration_12(env): def migration_12(env):
# Upgrading to Carddav Roundcube plugin to version 3+, it requires the carddav_* # Upgrading to Carddav Roundcube plugin to version 3+, it requires the carddav_*
# tables to be dropped. # tables to be dropped.
# Checking that the roundcube database already exists. # Checking that the roundcube database already exists.
if os.path.exists(os.path.join(env["STORAGE_ROOT"], "mail/roundcube/roundcube.sqlite")): if os.path.exists(os.path.join(env["STORAGE_ROOT"], "mail/roundcube/roundcube.sqlite")):
import sqlite3 import sqlite3
conn = sqlite3.connect(os.path.join(env["STORAGE_ROOT"], "mail/roundcube/roundcube.sqlite")) conn = sqlite3.connect(os.path.join(env["STORAGE_ROOT"], "mail/roundcube/roundcube.sqlite"))
c = conn.cursor() c = conn.cursor()
# Get a list of all the tables that begin with 'carddav_' # Get a list of all the tables that begin with 'carddav_'
c.execute("SELECT name FROM sqlite_master WHERE type = ? AND name LIKE ?", ('table', 'carddav_%')) c.execute("SELECT name FROM sqlite_master WHERE type = ? AND name LIKE ?", ('table', 'carddav_%'))
carddav_tables = c.fetchall() carddav_tables = c.fetchall()
# If there were tables that begin with 'carddav_', drop them # If there were tables that begin with 'carddav_', drop them
if carddav_tables: if carddav_tables:
for table in carddav_tables: for table in carddav_tables:
try: try:
table = table[0] table = table[0]
c = conn.cursor() c = conn.cursor()
dropcmd = "DROP TABLE %s" % table dropcmd = "DROP TABLE %s" % table
c.execute(dropcmd) c.execute(dropcmd)
except: except:
print("Failed to drop table", table) print("Failed to drop table", table)
# Save. # Save.
conn.commit() conn.commit()
conn.close() conn.close()
# Delete all sessions, requiring users to login again to recreate carddav_* # Delete all sessions, requiring users to login again to recreate carddav_*
# databases # databases
conn = sqlite3.connect(os.path.join(env["STORAGE_ROOT"], "mail/roundcube/roundcube.sqlite")) conn = sqlite3.connect(os.path.join(env["STORAGE_ROOT"], "mail/roundcube/roundcube.sqlite"))
c = conn.cursor() c = conn.cursor()
c.execute("delete from session;") c.execute("delete from session;")
conn.commit() conn.commit()
conn.close() conn.close()
def migration_13(env): def migration_13(env):
# Add the "mfa" table for configuring MFA for login to the control panel. # Add the "mfa" table for configuring MFA for login to the control panel.
@ -190,6 +190,13 @@ def migration_14(env):
db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite') db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite')
shell("check_call", ["sqlite3", db, "CREATE TABLE auto_aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);"]) shell("check_call", ["sqlite3", db, "CREATE TABLE auto_aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);"])
def migration_15(env):
# Replace PRIMARY_HOSTNAME with BOX_HOSTNAME in mailinabox.conf
shell("check_call", ["sed", "-i", "s/PRIMARY_HOSTNAME/BOX_HOSTNAME/g", "/etc/mailinabox.conf"])
env["BOX_HOSTNAME"] = env.get("PRIMARY_HOSTNAME", env.get("BOX_HOSTNAME"))
env["PRIMARY_HOSTNAME"] = None
del env["PRIMARY_HOSTNAME"]
########################################################### ###########################################################
def get_current_migration(): def get_current_migration():

View File

@ -24,12 +24,12 @@ includedir /etc/munin/munin-conf.d
cgiurl_graph /admin/munin/cgi-graph cgiurl_graph /admin/munin/cgi-graph
# a simple host tree # a simple host tree
[$PRIMARY_HOSTNAME] [$BOX_HOSTNAME]
address 127.0.0.1 address 127.0.0.1
# send alerts to the following address # send alerts to the following address
contacts admin contacts admin
contact.admin.command mail -s "Munin notification \${var:host}" administrator@$PRIMARY_HOSTNAME contact.admin.command mail -s "Munin notification \${var:host}" administrator@$BOX_HOSTNAME
contact.admin.always_send warning critical contact.admin.always_send warning critical
EOF EOF
@ -40,7 +40,7 @@ chown munin /var/log/munin/munin-cgi-graph.log
# ensure munin-node knows the name of this machine # ensure munin-node knows the name of this machine
# and reduce logging level to warning # and reduce logging level to warning
tools/editconf.py /etc/munin/munin-node.conf -s \ tools/editconf.py /etc/munin/munin-node.conf -s \
host_name="$PRIMARY_HOSTNAME" \ host_name="$BOX_HOSTNAME" \
log_level=1 log_level=1
# Update the activated plugins through munin's autoconfiguration. # Update the activated plugins through munin's autoconfiguration.

View File

@ -3,15 +3,15 @@
# the rest of the system setup so we may not yet have things installed. # the rest of the system setup so we may not yet have things installed.
apt_get_quiet install bind9-host sed netcat-openbsd apt_get_quiet install bind9-host sed netcat-openbsd
# Stop if the PRIMARY_HOSTNAME is listed in the Spamhaus Domain Block List. # Stop if the BOX_HOSTNAME is listed in the Spamhaus Domain Block List.
# The user might have chosen a name that was previously in use by a spammer # The user might have chosen a name that was previously in use by a spammer
# and will not be able to reliably send mail. Do this after any automatic # and will not be able to reliably send mail. Do this after any automatic
# choices made above. # choices made above.
if host "$PRIMARY_HOSTNAME.dbl.spamhaus.org" > /dev/null; then if host "$BOX_HOSTNAME.dbl.spamhaus.org" > /dev/null; then
echo echo
echo "The hostname you chose '$PRIMARY_HOSTNAME' is listed in the" echo "The hostname you chose '$BOX_HOSTNAME' is listed in the"
echo "Spamhaus Domain Block List. See http://www.spamhaus.org/dbl/" echo "Spamhaus Domain Block List. See http://www.spamhaus.org/dbl/"
echo "and http://www.spamhaus.org/query/domain/$PRIMARY_HOSTNAME." echo "and http://www.spamhaus.org/query/domain/$BOX_HOSTNAME."
echo echo
echo "You will not be able to send mail using this domain name, so" echo "You will not be able to send mail using this domain name, so"
echo "setup cannot continue." echo "setup cannot continue."

View File

@ -253,7 +253,7 @@ if [ ! -f "$STORAGE_ROOT/owncloud/owncloud.db" ]; then
mkdir -p "$STORAGE_ROOT/owncloud" mkdir -p "$STORAGE_ROOT/owncloud"
# Create an initial configuration file. # Create an initial configuration file.
instanceid=oc$(echo "$PRIMARY_HOSTNAME" | sha1sum | fold -w 10 | head -n 1) instanceid=oc$(echo "$BOX_HOSTNAME" | sha1sum | fold -w 10 | head -n 1)
cat > "$STORAGE_ROOT/owncloud/config.php" <<EOF; cat > "$STORAGE_ROOT/owncloud/config.php" <<EOF;
<?php <?php
\$CONFIG = array ( \$CONFIG = array (
@ -308,7 +308,7 @@ fi
# Update config.php. # Update config.php.
# * trusted_domains is reset to localhost by autoconfig starting with ownCloud 8.1.1, # * trusted_domains is reset to localhost by autoconfig starting with ownCloud 8.1.1,
# so set it here. It also can change if the box's PRIMARY_HOSTNAME changes, so # so set it here. It also can change if the box's BOX_HOSTNAME changes, so
# this will make sure it has the right value. # this will make sure it has the right value.
# * Some settings weren't included in previous versions of Mail-in-a-Box. # * Some settings weren't included in previous versions of Mail-in-a-Box.
# * We need to set the timezone to the system timezone to allow fail2ban to ban # * We need to set the timezone to the system timezone to allow fail2ban to ban
@ -325,10 +325,10 @@ include("$STORAGE_ROOT/owncloud/config.php");
\$CONFIG['config_is_read_only'] = false; \$CONFIG['config_is_read_only'] = false;
\$CONFIG['trusted_domains'] = array('$PRIMARY_HOSTNAME'); \$CONFIG['trusted_domains'] = array('$BOX_HOSTNAME');
\$CONFIG['memcache.local'] = '\OC\Memcache\APCu'; \$CONFIG['memcache.local'] = '\OC\Memcache\APCu';
\$CONFIG['overwrite.cli.url'] = 'https://${PRIMARY_HOSTNAME}/cloud'; \$CONFIG['overwrite.cli.url'] = 'https://${BOX_HOSTNAME}/cloud';
\$CONFIG['logtimezone'] = '$TIMEZONE'; \$CONFIG['logtimezone'] = '$TIMEZONE';
\$CONFIG['logdateformat'] = 'Y-m-d H:i:s'; \$CONFIG['logdateformat'] = 'Y-m-d H:i:s';
@ -342,8 +342,8 @@ include("$STORAGE_ROOT/owncloud/config.php");
), ),
); );
\$CONFIG['mail_domain'] = '$PRIMARY_HOSTNAME'; \$CONFIG['mail_domain'] = '$BOX_HOSTNAME';
\$CONFIG['mail_from_address'] = 'administrator'; # just the local part, matches the required administrator alias on mail_domain/$PRIMARY_HOSTNAME \$CONFIG['mail_from_address'] = 'administrator'; # just the local part, matches the required administrator alias on mail_domain/$BOX_HOSTNAME
\$CONFIG['mail_smtpmode'] = 'sendmail'; \$CONFIG['mail_smtpmode'] = 'sendmail';
\$CONFIG['mail_smtpauth'] = true; # if smtpmode is smtp \$CONFIG['mail_smtpauth'] = true; # if smtpmode is smtp
\$CONFIG['mail_smtphost'] = '127.0.0.1'; # if smtpmode is smtp \$CONFIG['mail_smtphost'] = '127.0.0.1'; # if smtpmode is smtp

View File

@ -26,8 +26,8 @@ if [ -z "${NONINTERACTIVE:-}" ]; then
fi fi
# The box needs a name. # The box needs a name.
if [ -z "${PRIMARY_HOSTNAME:-}" ]; then if [ -z "${BOX_HOSTNAME:-}" ]; then
if [ -z "${DEFAULT_PRIMARY_HOSTNAME:-}" ]; then if [ -z "${DEFAULT_BOX_HOSTNAME:-}" ]; then
# We recommend to use box.example.com as this hosts name. The # We recommend to use box.example.com as this hosts name. The
# domain the user possibly wants to use is example.com then. # domain the user possibly wants to use is example.com then.
# We strip the string "box." from the hostname to get the mail # We strip the string "box." from the hostname to get the mail
@ -66,19 +66,19 @@ you really want.
# Take the part after the @-sign as the user's domain name, and add # Take the part after the @-sign as the user's domain name, and add
# 'box.' to the beginning to create a default hostname for this machine. # 'box.' to the beginning to create a default hostname for this machine.
DEFAULT_PRIMARY_HOSTNAME=box.$(echo "$EMAIL_ADDR" | sed 's/.*@//') DEFAULT_BOX_HOSTNAME=box.$(echo "$EMAIL_ADDR" | sed 's/.*@//')
fi fi
input_box "Hostname" \ input_box "Hostname" \
"This box needs a name, called a 'hostname'. The name will form a part of the box's web address. "This box needs a name, called a 'hostname'. The name will form a part of the box's web address.
\n\nWe recommend that the name be a subdomain of the domain in your email \n\nWe recommend that the name be a subdomain of the domain in your email
address, so we're suggesting $DEFAULT_PRIMARY_HOSTNAME. address, so we're suggesting $DEFAULT_BOX_HOSTNAME.
\n\nYou can change it, but we recommend you don't. \n\nYou can change it, but we recommend you don't.
\n\nHostname:" \ \n\nHostname:" \
"$DEFAULT_PRIMARY_HOSTNAME" \ "$DEFAULT_BOX_HOSTNAME" \
PRIMARY_HOSTNAME BOX_HOSTNAME
if [ -z "$PRIMARY_HOSTNAME" ]; then if [ -z "$BOX_HOSTNAME" ]; then
# user hit ESC/cancel # user hit ESC/cancel
exit exit
fi fi
@ -181,8 +181,8 @@ if [ "$PUBLIC_IPV6" = "auto" ]; then
# Use a public API to get our public IPv6 address, or fall back to local network configuration. # Use a public API to get our public IPv6 address, or fall back to local network configuration.
PUBLIC_IPV6=$(get_publicip_from_web_service 6 || get_default_privateip 6) PUBLIC_IPV6=$(get_publicip_from_web_service 6 || get_default_privateip 6)
fi fi
if [ "$PRIMARY_HOSTNAME" = "auto" ]; then if [ "$BOX_HOSTNAME" = "auto" ]; then
PRIMARY_HOSTNAME=$(get_default_hostname) BOX_HOSTNAME=$(get_default_hostname)
fi fi
# Set STORAGE_USER and STORAGE_ROOT to default values (user-data and /home/user-data), unless # Set STORAGE_USER and STORAGE_ROOT to default values (user-data and /home/user-data), unless
@ -196,7 +196,7 @@ fi
# Show the configuration, since the user may have not entered it manually. # Show the configuration, since the user may have not entered it manually.
echo echo
echo "Primary Hostname: $PRIMARY_HOSTNAME" echo "Box Hostname: $BOX_HOSTNAME"
echo "Public IP Address: $PUBLIC_IP" echo "Public IP Address: $PUBLIC_IP"
if [ -n "$PUBLIC_IPV6" ]; then if [ -n "$PUBLIC_IPV6" ]; then
echo "Public IPv6 Address: $PUBLIC_IPV6" echo "Public IPv6 Address: $PUBLIC_IPV6"

View File

@ -77,42 +77,42 @@ tools/editconf.py /etc/spamassassin/local.cf -s \
# Our custom rules are added to their own file so that an update to the deb package config # Our custom rules are added to their own file so that an update to the deb package config
# does not remove our changes. # does not remove our changes.
# #
# We need to escape period's in $PRIMARY_HOSTNAME since spamassassin config uses regex. # We need to escape period's in $BOX_HOSTNAME since spamassassin config uses regex.
escapedprimaryhostname="${PRIMARY_HOSTNAME//./\\.}" escapedboxhostname="${BOX_HOSTNAME//./\\.}"
cat > /etc/spamassassin/miab_spf_dmarc.cf << EOF cat > /etc/spamassassin/miab_spf_dmarc.cf << EOF
# Evaluate DMARC Authentication-Results # Evaluate DMARC Authentication-Results
header DMARC_PASS Authentication-Results =~ /$escapedprimaryhostname; dmarc=pass/ header DMARC_PASS Authentication-Results =~ /$escapedboxhostname; dmarc=pass/
describe DMARC_PASS DMARC check passed describe DMARC_PASS DMARC check passed
score DMARC_PASS -0.1 score DMARC_PASS -0.1
header DMARC_NONE Authentication-Results =~ /$escapedprimaryhostname; dmarc=none/ header DMARC_NONE Authentication-Results =~ /$escapedboxhostname; dmarc=none/
describe DMARC_NONE DMARC record not found describe DMARC_NONE DMARC record not found
score DMARC_NONE 0.1 score DMARC_NONE 0.1
header DMARC_FAIL_NONE Authentication-Results =~ /$escapedprimaryhostname; dmarc=fail \(p=none/ header DMARC_FAIL_NONE Authentication-Results =~ /$escapedboxhostname; dmarc=fail \(p=none/
describe DMARC_FAIL_NONE DMARC check failed (p=none) describe DMARC_FAIL_NONE DMARC check failed (p=none)
score DMARC_FAIL_NONE 2.0 score DMARC_FAIL_NONE 2.0
header DMARC_FAIL_QUARANTINE Authentication-Results =~ /$escapedprimaryhostname; dmarc=fail \(p=quarantine/ header DMARC_FAIL_QUARANTINE Authentication-Results =~ /$escapedboxhostname; dmarc=fail \(p=quarantine/
describe DMARC_FAIL_QUARANTINE DMARC check failed (p=quarantine) describe DMARC_FAIL_QUARANTINE DMARC check failed (p=quarantine)
score DMARC_FAIL_QUARANTINE 5.0 score DMARC_FAIL_QUARANTINE 5.0
header DMARC_FAIL_REJECT Authentication-Results =~ /$escapedprimaryhostname; dmarc=fail \(p=reject/ header DMARC_FAIL_REJECT Authentication-Results =~ /$escapedboxhostname; dmarc=fail \(p=reject/
describe DMARC_FAIL_REJECT DMARC check failed (p=reject) describe DMARC_FAIL_REJECT DMARC check failed (p=reject)
score DMARC_FAIL_REJECT 10.0 score DMARC_FAIL_REJECT 10.0
# Evaluate SPF Authentication-Results # Evaluate SPF Authentication-Results
header SPF_PASS Authentication-Results =~ /$escapedprimaryhostname; spf=pass/ header SPF_PASS Authentication-Results =~ /$escapedboxhostname; spf=pass/
describe SPF_PASS SPF check passed describe SPF_PASS SPF check passed
score SPF_PASS -0.1 score SPF_PASS -0.1
header SPF_NONE Authentication-Results =~ /$escapedprimaryhostname; spf=none/ header SPF_NONE Authentication-Results =~ /$escapedboxhostname; spf=none/
describe SPF_NONE SPF record not found describe SPF_NONE SPF record not found
score SPF_NONE 2.0 score SPF_NONE 2.0
header SPF_FAIL Authentication-Results =~ /$escapedprimaryhostname; spf=fail/ header SPF_FAIL Authentication-Results =~ /$escapedboxhostname; spf=fail/
describe SPF_FAIL SPF check failed describe SPF_FAIL SPF check failed
score SPF_FAIL 5.0 score SPF_FAIL 5.0
EOF EOF

View File

@ -13,7 +13,7 @@
# * SMTP (opportunistic TLS for port 25 and submission on ports 465/587) # * SMTP (opportunistic TLS for port 25 and submission on ports 465/587)
# * HTTPS # * HTTPS
# #
# The certificate is created with its CN set to the PRIMARY_HOSTNAME. It is # The certificate is created with its CN set to the BOX_HOSTNAME. It is
# also used for other domains served over HTTPS until the user installs a # also used for other domains served over HTTPS until the user installs a
# better certificate for those domains. # better certificate for those domains.
# #
@ -74,10 +74,10 @@ if [ ! -f "$STORAGE_ROOT/ssl/ssl_certificate.pem" ]; then
CSR=/tmp/ssl_cert_sign_req-$$.csr CSR=/tmp/ssl_cert_sign_req-$$.csr
hide_output \ hide_output \
openssl req -new -key "$STORAGE_ROOT/ssl/ssl_private_key.pem" -out $CSR \ openssl req -new -key "$STORAGE_ROOT/ssl/ssl_private_key.pem" -out $CSR \
-sha256 -subj "/CN=$PRIMARY_HOSTNAME" -sha256 -subj "/CN=$BOX_HOSTNAME"
# Generate the self-signed certificate. # Generate the self-signed certificate.
CERT=$STORAGE_ROOT/ssl/$PRIMARY_HOSTNAME-selfsigned-$(date --rfc-3339=date | sed s/-//g).pem CERT=$STORAGE_ROOT/ssl/$BOX_HOSTNAME-selfsigned-$(date --rfc-3339=date | sed s/-//g).pem
hide_output \ hide_output \
openssl x509 -req -days 365 \ openssl x509 -req -days 365 \
-in $CSR -signkey "$STORAGE_ROOT/ssl/ssl_private_key.pem" -out "$CERT" -in $CSR -signkey "$STORAGE_ROOT/ssl/ssl_private_key.pem" -out "$CERT"

View File

@ -51,7 +51,7 @@ source setup/start.sh
EOF EOF
chmod +x /usr/local/bin/mailinabox chmod +x /usr/local/bin/mailinabox
# Ask the user for the PRIMARY_HOSTNAME, PUBLIC_IP, and PUBLIC_IPV6, # Ask the user for the BOX_HOSTNAME, PUBLIC_IP, and PUBLIC_IPV6,
# if values have not already been set in environment variables. When running # if values have not already been set in environment variables. When running
# non-interactively, be sure to set values for all! Also sets STORAGE_USER and # non-interactively, be sure to set values for all! Also sets STORAGE_USER and
# STORAGE_ROOT. # STORAGE_ROOT.
@ -60,7 +60,7 @@ source setup/questions.sh
# Run some network checks to make sure setup on this machine makes sense. # Run some network checks to make sure setup on this machine makes sense.
# Skip on existing installs since we don't want this to block the ability to # Skip on existing installs since we don't want this to block the ability to
# upgrade, and these checks are also in the control panel status checks. # upgrade, and these checks are also in the control panel status checks.
if [ -z "${DEFAULT_PRIMARY_HOSTNAME:-}" ]; then if [ -z "${DEFAULT_BOX_HOSTNAME:-}" ]; then
if [ -z "${SKIP_NETWORK_CHECKS:-}" ]; then if [ -z "${SKIP_NETWORK_CHECKS:-}" ]; then
source setup/network-checks.sh source setup/network-checks.sh
fi fi
@ -95,7 +95,7 @@ fi
cat > /etc/mailinabox.conf << EOF; cat > /etc/mailinabox.conf << EOF;
STORAGE_USER=$STORAGE_USER STORAGE_USER=$STORAGE_USER
STORAGE_ROOT=$STORAGE_ROOT STORAGE_ROOT=$STORAGE_ROOT
PRIMARY_HOSTNAME=$PRIMARY_HOSTNAME BOX_HOSTNAME=$BOX_HOSTNAME
PUBLIC_IP=$PUBLIC_IP PUBLIC_IP=$PUBLIC_IP
PUBLIC_IPV6=$PUBLIC_IPV6 PUBLIC_IPV6=$PUBLIC_IPV6
PRIVATE_IP=$PRIVATE_IP PRIVATE_IP=$PRIVATE_IP
@ -160,9 +160,9 @@ echo "Your Mail-in-a-Box is running."
echo echo
echo "Please log in to the control panel for further instructions at:" echo "Please log in to the control panel for further instructions at:"
echo echo
if management/status_checks.py --check-primary-hostname; then if management/status_checks.py --check-box-hostname; then
# Show the nice URL if it appears to be resolving and has a valid certificate. # Show the nice URL if it appears to be resolving and has a valid certificate.
echo "https://$PRIMARY_HOSTNAME/admin" echo "https://$BOX_HOSTNAME/admin"
echo echo
echo "If you have a DNS problem put the box's IP address in the URL" echo "If you have a DNS problem put the box's IP address in the URL"
echo "(https://$PUBLIC_IP/admin) but then check the TLS fingerprint:" echo "(https://$PUBLIC_IP/admin) but then check the TLS fingerprint:"

View File

@ -12,8 +12,8 @@ source setup/functions.sh # load our functions
# #
# First set the hostname in the configuration file, then activate the setting # First set the hostname in the configuration file, then activate the setting
echo "$PRIMARY_HOSTNAME" > /etc/hostname echo "$BOX_HOSTNAME" > /etc/hostname
hostname "$PRIMARY_HOSTNAME" hostname "$BOX_HOSTNAME"
# ### Fix permissions # ### Fix permissions

View File

@ -104,7 +104,7 @@ fi
mkdir -p /var/lib/mailinabox mkdir -p /var/lib/mailinabox
chmod a+rx /var/lib/mailinabox chmod a+rx /var/lib/mailinabox
cat conf/ios-profile.xml \ cat conf/ios-profile.xml \
| sed "s/PRIMARY_HOSTNAME/$PRIMARY_HOSTNAME/" \ | sed "s/BOX_HOSTNAME/$BOX_HOSTNAME/" \
| sed "s/UUID1/$(cat /proc/sys/kernel/random/uuid)/" \ | sed "s/UUID1/$(cat /proc/sys/kernel/random/uuid)/" \
| sed "s/UUID2/$(cat /proc/sys/kernel/random/uuid)/" \ | sed "s/UUID2/$(cat /proc/sys/kernel/random/uuid)/" \
| sed "s/UUID3/$(cat /proc/sys/kernel/random/uuid)/" \ | sed "s/UUID3/$(cat /proc/sys/kernel/random/uuid)/" \
@ -118,7 +118,7 @@ chmod a+r /var/lib/mailinabox/mobileconfig.xml
# https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat # https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat
# and https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration/FileFormat/HowTo. # and https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration/FileFormat/HowTo.
cat conf/mozilla-autoconfig.xml \ cat conf/mozilla-autoconfig.xml \
| sed "s/PRIMARY_HOSTNAME/$PRIMARY_HOSTNAME/" \ | sed "s/BOX_HOSTNAME/$BOX_HOSTNAME/" \
> /var/lib/mailinabox/mozilla-autoconfig.xml > /var/lib/mailinabox/mozilla-autoconfig.xml
chmod a+r /var/lib/mailinabox/mozilla-autoconfig.xml chmod a+r /var/lib/mailinabox/mozilla-autoconfig.xml
@ -130,10 +130,10 @@ chmod a+r /var/lib/mailinabox/mozilla-autoconfig.xml
# "MTA_STS_MODE=testing" which means "Messages will be delivered # "MTA_STS_MODE=testing" which means "Messages will be delivered
# as though there was no failure but a report will be sent if # as though there was no failure but a report will be sent if
# TLS-RPT is configured" if you are not sure you want this yet. Or "none". # TLS-RPT is configured" if you are not sure you want this yet. Or "none".
PUNY_PRIMARY_HOSTNAME=$(echo "$PRIMARY_HOSTNAME" | idn2) PUNY_BOX_HOSTNAME=$(echo "$BOX_HOSTNAME" | idn2)
cat conf/mta-sts.txt \ cat conf/mta-sts.txt \
| sed "s/MODE/${MTA_STS_MODE}/" \ | sed "s/MODE/${MTA_STS_MODE}/" \
| sed "s/PRIMARY_HOSTNAME/$PUNY_PRIMARY_HOSTNAME/" \ | sed "s/BOX_HOSTNAME/$PUNY_BOX_HOSTNAME/" \
> /var/lib/mailinabox/mta-sts.txt > /var/lib/mailinabox/mta-sts.txt
chmod a+r /var/lib/mailinabox/mta-sts.txt chmod a+r /var/lib/mailinabox/mta-sts.txt

View File

@ -132,7 +132,7 @@ cat > $RCM_CONFIG <<EOF;
), ),
); );
\$config['support_url'] = 'https://mailinabox.email/'; \$config['support_url'] = 'https://mailinabox.email/';
\$config['product_name'] = '$PRIMARY_HOSTNAME Webmail'; \$config['product_name'] = '$BOX_HOSTNAME Webmail';
\$config['cipher_method'] = 'AES-256-CBC'; # persistent login cookie and potentially other things \$config['cipher_method'] = 'AES-256-CBC'; # persistent login cookie and potentially other things
\$config['des_key'] = '$SECRET_KEY'; # 37 characters -> ~256 bits for AES-256, see above \$config['des_key'] = '$SECRET_KEY'; # 37 characters -> ~256 bits for AES-256, see above
\$config['plugins'] = array('html5_notifier', 'archive', 'zipdownload', 'password', 'managesieve', 'jqueryui', 'persistent_login', 'carddav'); \$config['plugins'] = array('html5_notifier', 'archive', 'zipdownload', 'password', 'managesieve', 'jqueryui', 'persistent_login', 'carddav');
@ -158,7 +158,7 @@ cat > ${RCM_PLUGIN_DIR}/carddav/config.inc.php <<EOF;
'name' => 'ownCloud', 'name' => 'ownCloud',
'username' => '%u', // login username 'username' => '%u', // login username
'password' => '%p', // login password 'password' => '%p', // login password
'url' => 'https://${PRIMARY_HOSTNAME}/cloud/remote.php/dav/addressbooks/users/%u/contacts/', 'url' => 'https://${BOX_HOSTNAME}/cloud/remote.php/dav/addressbooks/users/%u/contacts/',
'active' => true, 'active' => true,
'readonly' => false, 'readonly' => false,
'refresh_time' => '02:00:00', 'refresh_time' => '02:00:00',

View File

@ -41,7 +41,7 @@ if [ $needs_update == 1 ]; then
mv /tmp/z-push/*/src /usr/local/lib/z-push mv /tmp/z-push/*/src /usr/local/lib/z-push
rm -rf /tmp/z-push.zip /tmp/z-push rm -rf /tmp/z-push.zip /tmp/z-push
# Create admin and top scripts with PHP_VER # Create admin and top scripts with PHP_VER
rm -f /usr/sbin/z-push-{admin,top} rm -f /usr/sbin/z-push-{admin,top}
echo '#!/bin/bash' > /usr/sbin/z-push-admin echo '#!/bin/bash' > /usr/sbin/z-push-admin
echo php"$PHP_VER" /usr/local/lib/z-push/z-push-admin.php '"$@"' >> /usr/sbin/z-push-admin echo php"$PHP_VER" /usr/local/lib/z-push/z-push-admin.php '"$@"' >> /usr/sbin/z-push-admin
@ -49,7 +49,7 @@ if [ $needs_update == 1 ]; then
echo '#!/bin/bash' > /usr/sbin/z-push-top echo '#!/bin/bash' > /usr/sbin/z-push-top
echo php"$PHP_VER" /usr/local/lib/z-push/z-push-top.php '"$@"' >> /usr/sbin/z-push-top echo php"$PHP_VER" /usr/local/lib/z-push/z-push-top.php '"$@"' >> /usr/sbin/z-push-top
chmod 755 /usr/sbin/z-push-top chmod 755 /usr/sbin/z-push-top
echo $VERSION > /usr/local/lib/z-push/version echo $VERSION > /usr/local/lib/z-push/version
fi fi
@ -79,7 +79,7 @@ cp conf/zpush/backend_caldav.php /usr/local/lib/z-push/backend/caldav/config.php
# Configure Autodiscover # Configure Autodiscover
rm -f /usr/local/lib/z-push/autodiscover/config.php rm -f /usr/local/lib/z-push/autodiscover/config.php
cp conf/zpush/autodiscover_config.php /usr/local/lib/z-push/autodiscover/config.php cp conf/zpush/autodiscover_config.php /usr/local/lib/z-push/autodiscover/config.php
sed -i "s/PRIMARY_HOSTNAME/$PRIMARY_HOSTNAME/" /usr/local/lib/z-push/autodiscover/config.php sed -i "s/BOX_HOSTNAME/$BOX_HOSTNAME/" /usr/local/lib/z-push/autodiscover/config.php
sed -i "s^define('TIMEZONE', .*^define('TIMEZONE', '$(cat /etc/timezone)');^" /usr/local/lib/z-push/autodiscover/config.php sed -i "s^define('TIMEZONE', .*^define('TIMEZONE', '$(cat /etc/timezone)');^" /usr/local/lib/z-push/autodiscover/config.php
# Some directories it will use. # Some directories it will use.

View File

@ -11,22 +11,22 @@ import sys, re
import dns.reversename, dns.resolver import dns.reversename, dns.resolver
if len(sys.argv) < 3: if len(sys.argv) < 3:
print("Usage: tests/dns.py ipaddress hostname [primary hostname]") print("Usage: tests/dns.py ipaddress hostname [box hostname]")
sys.exit(1) sys.exit(1)
ipaddr, hostname = sys.argv[1:3] ipaddr, hostname = sys.argv[1:3]
primary_hostname = hostname box_hostname = hostname
if len(sys.argv) == 4: if len(sys.argv) == 4:
primary_hostname = sys.argv[3] box_hostname = sys.argv[3]
def test(server, description): def test(server, description):
tests = [ tests = [
(hostname, "A", ipaddr), (hostname, "A", ipaddr),
#(hostname, "NS", "ns1.%s.;ns2.%s." % (primary_hostname, primary_hostname)), #(hostname, "NS", "ns1.%s.;ns2.%s." % (box_hostname, box_hostname)),
("ns1." + primary_hostname, "A", ipaddr), ("ns1." + box_hostname, "A", ipaddr),
("ns2." + primary_hostname, "A", ipaddr), ("ns2." + box_hostname, "A", ipaddr),
("www." + hostname, "A", ipaddr), ("www." + hostname, "A", ipaddr),
(hostname, "MX", "10 " + primary_hostname + "."), (hostname, "MX", "10 " + box_hostname + "."),
(hostname, "TXT", '"v=spf1 mx -all"'), (hostname, "TXT", '"v=spf1 mx -all"'),
("mail._domainkey." + hostname, "TXT", '"v=DKIM1; k=rsa; s=email; " "p=__KEY__"'), ("mail._domainkey." + hostname, "TXT", '"v=DKIM1; k=rsa; s=email; " "p=__KEY__"'),
#("_adsp._domainkey." + hostname, "TXT", "\"dkim=all\""), #("_adsp._domainkey." + hostname, "TXT", "\"dkim=all\""),

View File

@ -462,7 +462,7 @@ class BashScript(Grammar):
v = v.replace("</pre>\n<pre class='shell'>", "") v = v.replace("</pre>\n<pre class='shell'>", "")
v = re.sub("<pre>([\w\W]*?)</pre>", lambda m : "<pre>" + strip_indent(m.group(1)) + "</pre>", v) v = re.sub("<pre>([\w\W]*?)</pre>", lambda m : "<pre>" + strip_indent(m.group(1)) + "</pre>", v)
v = re.sub(r"(\$?)PRIMARY_HOSTNAME", r"<b>box.yourdomain.com</b>", v) v = re.sub(r"(\$?)BOX_HOSTNAME", r"<b>box.yourdomain.com</b>", v)
v = re.sub(r"\$STORAGE_ROOT", r"<b>$STORE</b>", v) v = re.sub(r"\$STORAGE_ROOT", r"<b>$STORE</b>", v)
v = v.replace("`pwd`", "<code><b>/path/to/mailinabox</b></code>") v = v.replace("`pwd`", "<code><b>/path/to/mailinabox</b></code>")