Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ externals/
.vagrant
api/docs/api-docs.html
*.code-workspace
.vscode
.venv
4 changes: 2 additions & 2 deletions Vagrantfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Vagrant.configure("2") do |config|
# 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
# 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.provision :shell, :inline => <<-SH
Expand All @@ -18,7 +18,7 @@ Vagrant.configure("2") do |config|
export NONINTERACTIVE=1
export PUBLIC_IP=auto
export PUBLIC_IPV6=auto
export PRIMARY_HOSTNAME=auto
export BOX_HOSTNAME=auto
#export SKIP_NETWORK_CHECKS=1

# Start the setup script.
Expand Down
38 changes: 19 additions & 19 deletions conf/ios-profile.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,19 @@
<array>
<dict>
<key>CalDAVAccountDescription</key>
<string>PRIMARY_HOSTNAME calendar</string>
<string>BOX_HOSTNAME calendar</string>
<key>CalDAVHostName</key>
<string>PRIMARY_HOSTNAME</string>
<string>BOX_HOSTNAME</string>
<key>CalDAVPort</key>
<real>443</real>
<key>CalDAVUseSSL</key>
<true/>
<key>PayloadDescription</key>
<string>PRIMARY_HOSTNAME (Mail-in-a-Box)</string>
<string>BOX_HOSTNAME (Mail-in-a-Box)</string>
<key>PayloadDisplayName</key>
<string>PRIMARY_HOSTNAME calendar</string>
<string>BOX_HOSTNAME calendar</string>
<key>PayloadIdentifier</key>
<string>email.mailinabox.mobileconfig.PRIMARY_HOSTNAME.CalDAV</string>
<string>email.mailinabox.mobileconfig.BOX_HOSTNAME.CalDAV</string>
<key>PayloadOrganization</key>
<string></string>
<key>PayloadType</key>
Expand All @@ -37,33 +37,33 @@
</dict>
<dict>
<key>EmailAccountDescription</key>
<string>PRIMARY_HOSTNAME mail</string>
<string>BOX_HOSTNAME mail</string>
<key>EmailAccountType</key>
<string>EmailTypeIMAP</string>
<key>IncomingMailServerAuthentication</key>
<string>EmailAuthPassword</string>
<key>IncomingMailServerHostName</key>
<string>PRIMARY_HOSTNAME</string>
<string>BOX_HOSTNAME</string>
<key>IncomingMailServerPortNumber</key>
<integer>993</integer>
<key>IncomingMailServerUseSSL</key>
<true/>
<key>OutgoingMailServerAuthentication</key>
<string>EmailAuthPassword</string>
<key>OutgoingMailServerHostName</key>
<string>PRIMARY_HOSTNAME</string>
<string>BOX_HOSTNAME</string>
<key>OutgoingMailServerPortNumber</key>
<integer>465</integer>
<key>OutgoingMailServerUseSSL</key>
<true/>
<key>OutgoingPasswordSameAsIncomingPassword</key>
<true/>
<key>PayloadDescription</key>
<string>PRIMARY_HOSTNAME (Mail-in-a-Box)</string>
<string>BOX_HOSTNAME (Mail-in-a-Box)</string>
<key>PayloadDisplayName</key>
<string>PRIMARY_HOSTNAME mail</string>
<string>BOX_HOSTNAME mail</string>
<key>PayloadIdentifier</key>
<string>email.mailinabox.mobileconfig.PRIMARY_HOSTNAME.E-Mail</string>
<string>email.mailinabox.mobileconfig.BOX_HOSTNAME.E-Mail</string>
<key>PayloadOrganization</key>
<string></string>
<key>PayloadType</key>
Expand All @@ -81,21 +81,21 @@
</dict>
<dict>
<key>CardDAVAccountDescription</key>
<string>PRIMARY_HOSTNAME contacts</string>
<string>BOX_HOSTNAME contacts</string>
<key>CardDAVHostName</key>
<string>PRIMARY_HOSTNAME</string>
<string>BOX_HOSTNAME</string>
<key>CardDAVPort</key>
<integer>443</integer>
<key>CardDAVPrincipalURL</key>
<string>/cloud/remote.php/carddav/addressbooks/</string>
<key>CardDAVUseSSL</key>
<true/>
<key>PayloadDescription</key>
<string>PRIMARY_HOSTNAME (Mail-in-a-Box)</string>
<string>BOX_HOSTNAME (Mail-in-a-Box)</string>
<key>PayloadDisplayName</key>
<string>PRIMARY_HOSTNAME contacts</string>
<string>BOX_HOSTNAME contacts</string>
<key>PayloadIdentifier</key>
<string>email.mailinabox.mobileconfig.PRIMARY_HOSTNAME.carddav</string>
<string>email.mailinabox.mobileconfig.BOX_HOSTNAME.carddav</string>
<key>PayloadOrganization</key>
<string></string>
<key>PayloadType</key>
Expand All @@ -107,11 +107,11 @@
</dict>
</array>
<key>PayloadDescription</key>
<string>PRIMARY_HOSTNAME (Mail-in-a-Box)</string>
<string>BOX_HOSTNAME (Mail-in-a-Box)</string>
<key>PayloadDisplayName</key>
<string>PRIMARY_HOSTNAME</string>
<string>BOX_HOSTNAME</string>
<key>PayloadIdentifier</key>
<string>email.mailinabox.mobileconfig.PRIMARY_HOSTNAME</string>
<string>email.mailinabox.mobileconfig.BOX_HOSTNAME</string>
<key>PayloadOrganization</key>
<string></string>
<key>PayloadRemovalDisallowed</key>
Expand Down
22 changes: 11 additions & 11 deletions conf/mozilla-autoconfig.xml
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
<?xml version="1.0"?>
<clientConfig version="1.1">
<emailProvider id="PRIMARY_HOSTNAME">
<domain>PRIMARY_HOSTNAME</domain>
<emailProvider id="BOX_HOSTNAME">
<domain>BOX_HOSTNAME</domain>

<displayName>PRIMARY_HOSTNAME (Mail-in-a-Box)</displayName>
<displayShortName>PRIMARY_HOSTNAME</displayShortName>
<displayName>BOX_HOSTNAME (Mail-in-a-Box)</displayName>
<displayShortName>BOX_HOSTNAME</displayShortName>

<incomingServer type="imap">
<hostname>PRIMARY_HOSTNAME</hostname>
<hostname>BOX_HOSTNAME</hostname>
<port>993</port>
<socketType>SSL</socketType>
<username>%EMAILADDRESS%</username>
<authentication>password-cleartext</authentication>
</incomingServer>

<outgoingServer type="smtp">
<hostname>PRIMARY_HOSTNAME</hostname>
<hostname>BOX_HOSTNAME</hostname>
<port>465</port>
<socketType>SSL</socketType>
<username>%EMAILADDRESS%</username>
Expand All @@ -24,21 +24,21 @@
<useGlobalPreferredServer>false</useGlobalPreferredServer>
</outgoingServer>

<documentation url="https://PRIMARY_HOSTNAME/">
<descr lang="en">PRIMARY_HOSTNAME website.</descr>
<documentation url="https://BOX_HOSTNAME/">
<descr lang="en">BOX_HOSTNAME website.</descr>
</documentation>
</emailProvider>

<webMail>
<loginPage url="https://PRIMARY_HOSTNAME/mail/" />
<loginPageInfo url="https://PRIMARY_HOSTNAME/mail/" >
<loginPage url="https://BOX_HOSTNAME/mail/" />
<loginPageInfo url="https://BOX_HOSTNAME/mail/" >
<username>%EMAILADDRESS%</username>
<usernameField id="rcmloginuser" name="_user" />
<passwordField id="rcmloginpwd" name="_pass" />
<loginButton id="rcmloginsubmit" />
</loginPageInfo>
</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>
8 changes: 4 additions & 4 deletions conf/mta-sts.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version: STSv1
mode: MODE
mx: PRIMARY_HOSTNAME
max_age: 604800
version: STSv1
mode: MODE
mx: BOX_HOSTNAME
max_age: 604800
File renamed without changes.
2 changes: 1 addition & 1 deletion conf/postfix_outgoing_mail_header_filters
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# 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
# 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.
/^\s*User-Agent:/ IGNORE
Expand Down
2 changes: 1 addition & 1 deletion conf/zpush/autodiscover_config.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
// Defines the base path on the server
define('BASE_PATH', dirname($_SERVER['SCRIPT_FILENAME']). '/');

define('ZPUSH_HOST', 'PRIMARY_HOSTNAME');
define('ZPUSH_HOST', 'BOX_HOSTNAME');

define('USE_FULLEMAIL_FOR_LOGIN', true);

Expand Down
16 changes: 16 additions & 0 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
rtyaml
email_validator>=1.0.0
exclusiveprocess
flask
dnspython
python-dateutil
expiringdict
gunicorn
qrcode[pil]
pyotp
idna>=2.0.0
cryptography==37.0.2
psutil
postfix-mta-sts-resolver
b2sdk
boto3
2 changes: 1 addition & 1 deletion management/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ def index():


return render_template('index.html',
hostname=env['PRIMARY_HOSTNAME'],
hostname=env['BOX_HOSTNAME'],
storage_root=env['STORAGE_ROOT'],

no_users_exist=no_users_exist,
Expand Down
44 changes: 22 additions & 22 deletions management/dns_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@
def get_dns_domains(env):
# Add all domain names in use by email users and mail aliases, any
# 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 web_update import get_web_domains
domains = set()
domains |= set(get_mail_domains(env))
domains |= set(get_web_domains(env, include_www_redirects=False))
domains.add(env['PRIMARY_HOSTNAME'])
domains.add(env['BOX_HOSTNAME'])
return domains

def get_dns_zones(env):
Expand Down Expand Up @@ -144,10 +144,10 @@ def build_zones(env):
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

# 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.
for ns in ("ns1", "ns2"):
d = ns + "." + env["PRIMARY_HOSTNAME"]
d = ns + "." + env["BOX_HOSTNAME"]
domains.add(d)
auto_domains.add(d)

Expand All @@ -161,9 +161,9 @@ def build_zones(env):
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.
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.
additional_records = list(get_custom_dns_config(env))
Expand All @@ -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
# is managed outside of the box.
if is_zone:
# Obligatory NS record to ns1.PRIMARY_HOSTNAME.
records.append((None, "NS", "ns1.%s." % env["PRIMARY_HOSTNAME"], False))
# Obligatory NS record to ns1.BOX_HOSTNAME.
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
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)


# In PRIMARY_HOSTNAME...
if domain == env["PRIMARY_HOSTNAME"]:
# Set the A/AAAA records. Do this early for the PRIMARY_HOSTNAME so that the user cannot override them
# In BOX_HOSTNAME...
if domain == env["BOX_HOSTNAME"]:
# 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.
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."))
Expand Down Expand Up @@ -281,7 +281,7 @@ def has_rec(qname, rtype, prefix=None):
if domain_properties[domain]["mail"]:
# The MX record says where email for the domain should be delivered: Here!
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
# the domain, and no one else.
Expand All @@ -304,14 +304,14 @@ def has_rec(qname, rtype, prefix=None):
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"]:
# 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).
# 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"):
qname = "_" + dav + "davs._tcp"
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@"
# this domain name, then the domain name is a MTA-STS (https://tools.ietf.org/html/rfc8461)
Expand All @@ -324,15 +324,15 @@ def has_rec(qname, rtype, prefix=None):
#
# 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
# 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
# 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
# always set them (by including them in the www domains) --- only the TXT records depend on there
# being valid certificates.
mta_sts_records = [ ]
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):
# 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
Expand Down Expand Up @@ -479,7 +479,7 @@ def write_nsd_zone(domain, zonefile, records, env, force):
# 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 PRIMARY_HOSTNAME. Hopefully that's legit.
# @ the BOX_HOSTNAME. Hopefully that's legit.
#
# For the refresh through TTL fields, a good reference is:
# https://www.ripe.net/publications/docs/ripe-203
Expand All @@ -492,7 +492,7 @@ def write_nsd_zone(domain, zonefile, records, env, force):
$ORIGIN {domain}.
$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
7200 ; Refresh (secondary nameserver update interval)
3600 ; Retry (when refresh fails, how often to try again, should be lower than the refresh)
Expand All @@ -502,7 +502,7 @@ def write_nsd_zone(domain, zonefile, records, env, force):
"""

# 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.
for subdomain, querytype, value, _explanation in records:
Expand Down
6 changes: 3 additions & 3 deletions management/email_administrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
subject = sys.argv[1]

# Administrator's email address.
admin_addr = "administrator@" + env['PRIMARY_HOSTNAME']
admin_addr = "administrator@" + env['BOX_HOSTNAME']

# Read in STDIN.
content = sys.stdin.read().strip()
Expand All @@ -37,9 +37,9 @@
# In Python 3.6:
#msg = Message()

msg['From'] = '"{}" <{}>'.format(env['PRIMARY_HOSTNAME'], admin_addr)
msg['From'] = '"{}" <{}>'.format(env['BOX_HOSTNAME'], 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>'

Expand Down
Loading