utils.py 6.08 KB
Newer Older
1 2 3 4
import os.path

CONF_DIR = os.path.join(os.path.dirname(__file__), "../conf")

5 6
def load_environment():
    # Load settings from /etc/mailinabox.conf.
7
    return load_env_vars_from_file("/etc/mailinabox.conf")
Joshua Tauberer's avatar
Joshua Tauberer committed
8 9 10

def load_env_vars_from_file(fn):
    # Load settings from a KEY=VALUE file.
11 12
    import collections
    env = collections.OrderedDict()
Joshua Tauberer's avatar
Joshua Tauberer committed
13 14
    for line in open(fn): env.setdefault(*line.strip().split("=", 1))
    return env
15

16 17 18 19 20
def save_environment(env):
    with open("/etc/mailinabox.conf", "w") as f:
        for k, v in env.items():
            f.write("%s=%s\n" % (k, v))

21 22 23 24 25
def safe_domain_name(name):
    # Sanitize a domain name so it is safe to use as a file name on disk.
    import urllib.parse
    return urllib.parse.quote(name, safe='')

26 27 28 29
def unsafe_domain_name(name_encoded):
    import urllib.parse
    return urllib.parse.unquote(name_encoded)

30
def sort_domains(domain_names, env):
31
    # Put domain names in a nice sorted order. For web_update, PRIMARY_HOSTNAME
32 33
    # must appear first so it becomes the nginx default server.
    
34
    # First group PRIMARY_HOSTNAME and its subdomains, then parent domains of PRIMARY_HOSTNAME, then other domains.
35 36
    groups = ( [], [], [] )
    for d in domain_names:
37
        if d == env['PRIMARY_HOSTNAME'] or d.endswith("." + env['PRIMARY_HOSTNAME']):
38
            groups[0].append(d)
39
        elif env['PRIMARY_HOSTNAME'].endswith("." + d):
40 41 42 43 44 45 46
            groups[1].append(d)
        else:
            groups[2].append(d)

    # Within each group, sort parent domains before subdomains and after that sort lexicographically.
    def sort_group(group):
        # Find the top-most domains.
47
        top_domains = sorted(d for d in group if len([s for s in group if d.endswith("." + s)]) == 0)
48 49 50 51 52 53 54 55 56 57
        ret = []
        for d in top_domains:
            ret.append(d)
            ret.extend( sort_group([s for s in group if s.endswith("." + d)]) )
        return ret
        
    groups = [sort_group(g) for g in groups]

    return groups[0] + groups[1] + groups[2]

58 59 60 61 62 63 64 65 66 67 68
def sort_email_addresses(email_addresses, env):
    email_addresses = set(email_addresses)
    domains = set(email.split("@", 1)[1] for email in email_addresses if "@" in email)
    ret = []
    for domain in sort_domains(domains, env):
        domain_emails = set(email for email in email_addresses if email.endswith("@" + domain))
        ret.extend(sorted(domain_emails))
        email_addresses -= domain_emails
    ret.extend(sorted(email_addresses)) # whatever is left
    return ret

69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
def exclusive_process(name):
    # Ensure that a process named `name` does not execute multiple
    # times concurrently.
    import os, sys, atexit
    pidfile = '/var/run/mailinabox-%s.pid' % name
    mypid = os.getpid()

    # Attempt to get a lock on ourself so that the concurrency check
    # itself is not executed in parallel.
    with open(__file__, 'r+') as flock:
        # Try to get a lock. This blocks until a lock is acquired. The
        # lock is held until the flock file is closed at the end of the
        # with block.
        os.lockf(flock.fileno(), os.F_LOCK, 0)

        # While we have a lock, look at the pid file. First attempt
        # to write our pid to a pidfile if no file already exists there.
        try:
            with open(pidfile, 'x') as f:
                # Successfully opened a new file. Since the file is new
                # there is no concurrent process. Write our pid.
                f.write(str(mypid))
                atexit.register(clear_my_pid, pidfile)
                return
        except FileExistsError:
            # The pid file already exixts, but it may contain a stale
            # pid of a terminated process.
            with open(pidfile, 'r+') as f:
                # Read the pid in the file.
                existing_pid = None
                try:
                    existing_pid = int(f.read().strip())
                except ValueError:
                    pass # No valid integer in the file.

                # Check if the pid in it is valid.
                if existing_pid:
                    if is_pid_valid(existing_pid):
                        print("Another %s is already running (pid %d)." % (name, existing_pid), file=sys.stderr)
                        sys.exit(1)

                # Write our pid.
                f.seek(0)
                f.write(str(mypid))
                f.truncate()
                atexit.register(clear_my_pid, pidfile)
 

def clear_my_pid(pidfile):
    import os
    os.unlink(pidfile)


def is_pid_valid(pid):
    """Checks whether a pid is a valid process ID of a currently running process."""
    # adapted from http://stackoverflow.com/questions/568271/how-to-check-if-there-exists-a-process-with-a-given-pid
    import os, errno
    if pid <= 0: raise ValueError('Invalid PID.')
    try:
        os.kill(pid, 0)
    except OSError as err:
        if err.errno == errno.ESRCH: # No such process
            return False
        elif err.errno == errno.EPERM: # Not permitted to send signal
            return True
        else: # EINVAL
            raise
    else:
137 138
        return True

139
def shell(method, cmd_args, env={}, capture_stderr=False, return_bytes=False, trap=False, input=None):
140 141 142
    # A safe way to execute processes.
    # Some processes like apt-get require being given a sane PATH.
    import subprocess
143

144
    env.update({ "PATH": "/sbin:/bin:/usr/sbin:/usr/bin" })
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
    kwargs = {
        'env': env,
        'stderr': None if not capture_stderr else subprocess.STDOUT,
    }
    if method == "check_output" and input is not None:
        kwargs['input'] = input

    if not trap:
        ret = getattr(subprocess, method)(cmd_args, **kwargs)
    else:
        try:
            ret = getattr(subprocess, method)(cmd_args, **kwargs)
            code = 0
        except subprocess.CalledProcessError as e:
            ret = e.output
            code = e.returncode
161
    if not return_bytes and isinstance(ret, bytes): ret = ret.decode("utf8")
162 163 164 165
    if not trap:
        return ret
    else:
        return code, ret
166 167 168 169 170 171

def create_syslog_handler():
    import logging.handlers
    handler = logging.handlers.SysLogHandler(address='/dev/log')
    handler.setLevel(logging.WARNING)
    return handler