#!/usr/bin/python3 # # Generate documentation for how this machine works by # parsing our bash scripts! import cgi, re import markdown from modgrammar import * def generate_documentation(): print(""" Build Your Own Mail Server From Scratch

Build Your Own Mail Server From Scratch

Here’s how you can build your own mail server from scratch. This document is generated automatically from our setup script.


""") parser = Source.parser() for line in open("setup/start.sh"): try: fn = parser.parse_string(line).filename() except: continue if fn in ("setup/preflight.sh", "setup/questions.sh", "setup/firstuser.sh", "setup/management.sh"): continue import sys print(fn, file=sys.stderr) print(BashScript.parse(fn)) print(""" """) class HashBang(Grammar): grammar = (L('#!'), REST_OF_LINE, EOL) def value(self): return "" def strip_indent(s): lines = s.split("\n") min_indent = min(len(re.match(r"\s*", line).group(0)) for line in lines if len(line) > 0) lines = [line[min_indent:] for line in lines] return "\n".join(lines) class Comment(Grammar): grammar = ONE_OR_MORE(ZERO_OR_MORE(SPACE), L('#'), REST_OF_LINE, EOL) def value(self): if self.string.replace("#", "").strip() == "": return "\n" lines = [x[2].string for x in self[0]] content = "\n".join(lines) content = strip_indent(content) return markdown.markdown(content, output_format="html4") + "\n\n" FILENAME = WORD('a-z0-9-/.') class Source(Grammar): grammar = ((L('.') | L('source')), L(' '), FILENAME, Comment | EOL) def filename(self): return self[2].string.strip() def value(self): return BashScript.parse(self.filename()) class CatEOF(Grammar): grammar = (ZERO_OR_MORE(SPACE), L('cat > '), ANY_EXCEPT(WHITESPACE), L(" <<"), OPTIONAL(SPACE), L("EOF;"), EOL, REPEAT(ANY, greedy=False), EOL, L("EOF"), EOL) def value(self): content = self[7].string content = re.sub(r"\\([$])", r"\1", content) # un-escape bash-escaped characters return "
overwrite
" + self[2].string + "
" + cgi.escape(content) + "
\n" class HideOutput(Grammar): grammar = (L("hide_output "), REF("BashElement")) def value(self): return self[1].value() class SuppressedLine(Grammar): grammar = (OPTIONAL(SPACE), L("echo "), REST_OF_LINE, EOL) def value(self): if "|" in self.string or ">" in self.string: return "
" + cgi.escape(self.string.strip()) + "
\n" return "" class EditConf(Grammar): grammar = ( L('tools/editconf.py '), FILENAME, SPACE, OPTIONAL((LIST_OF( L("-w") | L("-s") | L("-c ';'"), sep=SPACE, ), SPACE)), REST_OF_LINE, OPTIONAL(SPACE), EOL ) def value(self): conffile = self[1] options = [""] mode = 1 for c in self[4].string: if mode == 1 and c in (" ", "\t") and options[-1] != "": # new word options.append("") elif mode < 0: # escaped character options[-1] += c mode = -mode elif c == "\\": # escape next character mode = -mode elif mode == 1 and c == '"': mode = 2 elif mode == 2 and c == '"': mode = 1 else: options[-1] += c if options[-1] == "": options.pop(-1) return "
additional settings for
" + self[1].string + "
" + "\n".join(cgi.escape(s) for s in options) + "
\n" class CaptureOutput(Grammar): grammar = OPTIONAL(SPACE), WORD("A-Za-z_"), L('=$('), REST_OF_LINE, L(")"), OPTIONAL(L(';')), EOL def value(self): cmd = self[3].string cmd = cmd.replace("; ", "\n") return "
$" + self[1].string + "=
" + cgi.escape(cmd) + "
\n" class SedReplace(Grammar): grammar = OPTIONAL(SPACE), L('sed -i "s/'), OPTIONAL(L('^')), ONE_OR_MORE(WORD("-A-Za-z0-9 #=\\{};.*$_!()")), L('/'), ONE_OR_MORE(WORD("-A-Za-z0-9 #=\\{};.*$_!()")), L('/"'), SPACE, FILENAME, EOL def value(self): return "
edit
" + self[8].string + "

replace

" + cgi.escape(self[3].string.replace(".*", ". . .")) + "

with

" + cgi.escape(self[5].string.replace("\\n", "\n").replace("\\t", "\t")) + "
\n" def shell_line(bash): return "
" + cgi.escape(wrap_lines(bash.strip())) + "
\n" class AptGet(Grammar): grammar = (ZERO_OR_MORE(SPACE), L("apt_install "), REST_OF_LINE, EOL) def value(self): return shell_line("apt-get install -y " + re.sub(r"\s+", " ", self[2].string)) class UfwAllow(Grammar): grammar = (ZERO_OR_MORE(SPACE), L("ufw_allow "), REST_OF_LINE, EOL) def value(self): return shell_line("ufw allow " + self[2].string) class RestartService(Grammar): grammar = (ZERO_OR_MORE(SPACE), L("restart_service "), REST_OF_LINE, EOL) def value(self): return shell_line("service " + self[2].string + " restart") class OtherLine(Grammar): grammar = (REST_OF_LINE, EOL) def value(self): if self.string.strip() == "": return "" return "
" + cgi.escape(self.string.rstrip()) + "
\n" class BashElement(Grammar): grammar = Comment | Source | CatEOF | SuppressedLine | HideOutput | EditConf | CaptureOutput | SedReplace | AptGet | UfwAllow | RestartService | OtherLine def value(self): return self[0].value() class BashScript(Grammar): grammar = (OPTIONAL(HashBang), REPEAT(BashElement)) def value(self): return [line.value() for line in self[1]] @staticmethod def parse(fn): if fn in ("setup/functions.sh", "/etc/mailinabox.conf"): return "" parser = BashScript.parser() string = open(fn).read() string = re.sub(r"\s*\\\n\s*", " ", string) string = re.sub(".* #NODOC\n", "", string) string = re.sub("\n\s*if .*|\n\s*fi|\n\s*else|\n\s*elif .*", "", string) string = re.sub("hide_output ", "", string) result = parser.parse_string(string) v = "\n" % ("https://github.com/mail-in-a-box/mailinabox/tree/master/" + fn, fn) v += "".join(result.value()) v = v.replace("\n
", "")
		v = re.sub("
([\w\W]*?)
", lambda m : "
" + strip_indent(m.group(1)) + "
", v) v = re.sub(r"\$?PRIMARY_HOSTNAME", "box.yourdomain.com", v) v = re.sub(r"\$?STORAGE_ROOT", "/path/to/user-data", v) v = v.replace("`pwd`", "/path/to/mailinabox") return v def wrap_lines(text, cols=60): ret = "" words = re.split("(\s+)", text) linelen = 0 for w in words: if linelen + len(w) > cols-1: ret += " \\\n" ret += " " linelen = 0 if linelen == 0 and w.strip() == "": continue ret += w linelen += len(w) return ret if __name__ == '__main__': generate_documentation()