All files / Scripts HeaderModel.ts

90.1% Statements 82/91
74.46% Branches 35/47
90.9% Functions 10/11
93.5% Lines 72/77

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 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 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 1623x 3x 3x 3x 3x 3x   3x 3x 3x   3x                   108x 27x         41x 41x 41x 41x 41x 41x 41x 41x 41x       41x   41x 31x 31x 31x     41x       31x 31x           34x   34x 34x   34x 34x 34x       55x 55x 13x 13x                   42x             42x 38x 38x 38x   4x     4x 4x 4x 4x 4x                       34x       34x       38x     34x           31x 31x   31x 31x     31x   34x     21x 20x 20x 1x     31x       31x 31x 31x 31x 31x 31x 31x      
import { Decoder } from "./2047";
import { Poster } from "./Poster";
import { AntiSpamReport } from "./row/Antispam";
import { ForefrontAntiSpamReport } from "./row/ForefrontAntispam";
import { Header } from "./row/Header";
import { rulesService } from "./rules";
import { ViolationGroup } from "./rules/types/AnalysisTypes";
import { Summary } from "./Summary";
import { Other } from "./table/Other";
import { Received } from "./table/Received";
 
export class HeaderModel {
    public originalHeaders: string;
    public summary: Summary;
    public receivedHeaders: Received;
    public forefrontAntiSpamReport: ForefrontAntiSpamReport;
    public antiSpamReport: AntiSpamReport;
    public otherHeaders: Other;
    public violationGroups: ViolationGroup[];
    private hasDataInternal: boolean;
    private statusInternal: string;
    public get hasData(): boolean { return this.hasDataInternal || !!this.statusInternal; }
    public get status(): string { return this.statusInternal; }
    public set status(value) { this.statusInternal = value; }
    [index: string]: unknown;
 
    private constructor() {
        this.summary = new Summary();
        this.receivedHeaders = new Received();
        this.forefrontAntiSpamReport = new ForefrontAntiSpamReport();
        this.antiSpamReport = new AntiSpamReport();
        this.otherHeaders = new Other();
        this.originalHeaders = "";
        this.statusInternal = "";
        this.hasDataInternal = false;
        this.violationGroups = [];
    }
 
    public static async create(headers?: string): Promise<HeaderModel> {
        const model = new HeaderModel();
 
        if (headers) {
            model.parseHeaders(headers);
            await model.analyzeRules();
            Poster.postMessageToParent("modelToString", model.toString());
        }
 
        return model;
    }
 
    private async analyzeRules(): Promise<void> {
        const validationResult = await rulesService.analyzeHeaders(this);
        this.violationGroups = validationResult.violationGroups;
    }
 
    public static getHeaderList(headers: string): Header[] {
        // First, break up out input by lines.
        // Keep empty lines for recognizing the boundary between the header section & the body.
        const lines: string[] = headers.split(/\r\n|\r|\n/);
 
        const headerList: Header[] = [];
        let iNextHeader = 0;
        let prevHeader: Header | undefined;
        let body = false;
        headerSection: while (!body) {
            unfoldLines: for (let line of lines) {
                // Handling empty lines. The body is separated from the header section by an empty line (RFC 5322, 2.1).
                // To avoid processing the body as headers we should stop there, as someone might paste an entire message.
                // Empty lines at the beginning can be omitted, because that could be a common copy-paste error.
                Iif (body) break headerSection;
                if (line === "") {
                    Eif (headerList.length > 0) body = true;
                    continue unfoldLines;
                }
 
                // Recognizing a header:
                // - First colon comes before first white space.
                // - We're not strictly honoring white space folding because initial white space
                // - is commonly lost. Instead, we heuristically assume that space before a colon must have been folded.
                // This expression will give us:
                // match[1] - everything before the first colon, assuming no spaces (header).
                // match[2] - everything after the first colon (value).
                const match: RegExpMatchArray | null = line.match(/(^[\w-.]*?): ?(.*)/);
 
                // There's one false positive we might get: if the time in a Received header has been
                // folded to the next line, the line might start with something like "16:20:05 -0400".
                // This matches our regular expression. The RFC does not preclude such a header, but I've
                // never seen one in practice, so we check for and exclude 'headers' that
                // consist only of 1 or 2 digits.
                if (match && match[1] && !match[1].match(/^\d{1,2}$/)) {
                    headerList[iNextHeader] = new Header(match[1], match[2] ?? "");
                    prevHeader = headerList[iNextHeader];
                    iNextHeader++;
                } else {
                    if (iNextHeader > 0) {
                        // Tack this line to the previous line
                        // All folding whitespace should collapse to a single space
                        line = line.replace(/^[\s]+/, "");
                        Iif (!line) continue unfoldLines;
                        Eif (prevHeader) {
                            const separator: string = prevHeader.value ? " " : "";
                            prevHeader.value += separator + line;
                        }
                    } else E{
                        // If we didn't have a previous line, go ahead and use this line
                        if (line.match(/\S/g)) {
                            headerList[iNextHeader] = new Header("", line);
                            prevHeader = headerList[iNextHeader];
                            iNextHeader++;
                        }
                    }
                }
            }
            break headerSection;
        }
 
        // 2047 decode our headers now
        headerList.forEach((header: Header) => {
            // Clean 2047 encoding
            // Strip nulls
            // Strip trailing carriage returns
            header.value = Decoder.clean2047Encoding(header.value).replace(/\0/g, "").replace(/[\n\r]+$/, "");
        });
 
        return headerList;
    }
 
    public parseHeaders(headers: string): void {
        // Initialize originalHeaders in case we have parsing problems
        // Flatten CRLF to LF to avoid extra blank lines
        this.originalHeaders = headers.replace(/(?:\r\n|\r|\n)/g, "\n");
        const headerList: Header[] = HeaderModel.getHeaderList(headers);
 
        Eif (headerList.length > 0) {
            this.hasDataInternal = true;
        }
 
        headerList.forEach((header: Header) => {
            // Grab values for our summary pane
            if (this.summary.add(header)) return;
 
            // Properties with special parsing
            if (this.forefrontAntiSpamReport.add(header)) return;
            Iif (this.antiSpamReport.add(header)) return;
            if (this.receivedHeaders.add(header)) return;
            this.otherHeaders.add(header);
        });
 
        this.summary.totalTime = this.receivedHeaders.computeDeltas();
    }
 
    public toString(): string {
        const ret: string[] = [];
        if (this.summary.exists()) ret.push(this.summary.toString());
        if (this.receivedHeaders.exists()) ret.push(this.receivedHeaders.toString());
        if (this.forefrontAntiSpamReport.exists()) ret.push(this.forefrontAntiSpamReport.toString());
        Iif (this.antiSpamReport.exists()) ret.push(this.antiSpamReport.toString());
        if (this.otherHeaders.exists()) ret.push(this.otherHeaders.toString());
        return ret.join("\n\n");
    }
}