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                   96x 24x         40x 40x 40x 40x 40x 40x 40x 40x 40x       40x   40x 30x 30x 30x     40x       30x 30x           33x   33x 33x   33x 33x 33x       54x 54x 13x 13x                   41x             41x 37x 37x 37x   4x     4x 4x 4x 4x 4x                       33x       33x       37x     33x           30x 30x   30x 30x     30x   33x     20x 19x 19x 1x     30x       30x 30x 30x 30x 30x 30x 30x      
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");
    }
}