#!/bin/env python # Simple text-wrapping script for email. # Preserves code blocks, quotes, and signature. # Automatically joins and re-wraps paragraphs to # ensure even spacing & avoid ugly wrapping. # Preserves signoffs, eg: Yours truly,\nJohn Doe # Signoff heuristic: # 1-5 words ending with a comma, followed by # 1-5 words that each start with capital letters. # Author: Daniel Fichtinger # License: MIT import textwrap import sys import re import argparse paragraph: list[str] = [] skipping = False squash = True prev_is_parbreak = False out_stream = sys.stdout reflow = True width = 74 break_long_words = False replace_whitespace = True def pprint(string: str): if not squash: print(string, file=out_stream) else: parbreak = not string global prev_is_parbreak if skipping or not (parbreak and prev_is_parbreak): print(string, file=out_stream) prev_is_parbreak = parbreak def wrap(text: str): return textwrap.wrap( text, width=width, break_long_words=break_long_words, replace_whitespace=replace_whitespace, ) def flush_paragraph(): if paragraph: if reflow: joined = " ".join(paragraph) wrapped = wrap(joined) pprint("\n".join(wrapped)) else: for line in paragraph: for wrapped_line in wrap(line): pprint(wrapped_line) paragraph.clear() signoff_cache: str = "" def check_signoff(line: str) -> bool: if not line: return False words = line.split() n = len(words) # first potential signoff line if not signoff_cache and 1 <= n <= 5 and line[-1] == ",": return True # second potential line elif signoff_cache and 1 <= n <= 5 and line[-1].isalpha(): for w in words: if not w[0].isupper(): return False return True else: return False if __name__ == "__main__": parser = argparse.ArgumentParser( description='Formatter for plain text email.\n"--no-*" options are NOT passed by default.', epilog=""" Author : Daniel Fichtinger License: MIT Contact: daniel@ficd.ca """, formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( "-w", "--width", required=False, help="Text width for wrapping. (default: %(default)s)", default=width, type=int, ) parser.add_argument( "-b", "--break-long-words", required=False, help="Break long words while wrapping. (default: %(default)s)", action="store_true", ) parser.add_argument( "--no-replace-whitespace", required=False, help="Don't normalize whitespace when wrapping.", action="store_false", ) parser.add_argument( "--no-reflow", required=False, help="Don't reflow lines.", action="store_false", ) parser.add_argument( "--no-signoff", required=False, help="Don't preserve signoff line breaks.", action="store_false", ) parser.add_argument( "--no-signature", required=False, help="Don't preserve signature block.", action="store_false", ) parser.add_argument( "--no-squash", required=False, help="Don't squash consecutive paragraph breaks.", action="store_false", ) parser.add_argument( "-i", "--input", required=False, type=str, default="STDIN", help="Input file. (default: %(default)s)", ) parser.add_argument( "-o", "--output", required=False, type=str, default="STDOUT", help="Output file. (default: %(default)s)", ) args = parser.parse_args() width = args.width should_check_signoff = args.no_signoff should_check_signature = args.no_signature reflow = args.no_reflow squash = args.no_squash replace_whitespace = args.no_replace_whitespace break_long_words = args.break_long_words if args.input == "STDIN": reader = sys.stdin else: with open(args.input, "r") as in_stream: reader = in_stream if args.output != "STDOUT": out_stream = open(args.output, "w") for line in reader: line = line.rstrip() if should_check_signoff: is_signoff = check_signoff(line) if is_signoff: if not signoff_cache: signoff_cache = line else: pprint(signoff_cache) pprint(line) signoff_cache = "" continue elif not is_signoff and signoff_cache: paragraph.append(signoff_cache) signoff_cache = "" if line.startswith("```"): flush_paragraph() skipping = not skipping pprint(line) elif should_check_signature and line == "--": flush_paragraph() skipping = True pprint("-- ") elif not line or re.match( r"^(\s+|-\s+|\+\s+|\*\s+|>\s*|#\s+|From:|To:|Cc:|Bcc:|Subject:|Reply-To:|In-Reply-To:|References:|Date:|Message-Id:|User-Agent:)", line, ): flush_paragraph() pprint(line) elif skipping: pprint(line) else: paragraph.append(line) else: if signoff_cache: paragraph.append(signoff_cache) signoff_cache = "" flush_paragraph() if out_stream is not None: out_stream.close() # pyright: basic