/**
* Class that holds information about one "Received:" header line.
* Visit these RFCs for more information:
* RFC 5321 section 4.4
* RFC 4021 section 3.6.7
* RFC 2822 section 3.6.7
* RFC 2821 section 4.4
*/
export class Received {
/// The date of this received line.
/// Is if not present in the received header line.
date: number // TODO: DateTime
/// A dictionary that contains the names and values of the
/// received header line.
/// If the received header is invalid and contained one name
/// multiple times, the first one is used and the rest is ignored.
//
/// If the header lines looks like:
///
/// from sending.com (localMachine [127.0.0.1]) by test.net (Postfix)
///
/// then the dictionary will contain two keys: "from" and "by" with the values
/// "sending.com (localMachine [127.0.0.1])" and "test.net (Postfix)".$
names: Record
/// The raw input string that was parsed into this class.
raw: string
/// Parses a Received header value.
///
/// The value for the header to be parsed
///
/// If is
///
constructor(headerValue: string) {
if (headerValue == null) throw new Error("headerValue must be a string!")
this.raw = headerValue
this.date = 0
if (headerValue.includes(";")) {
const semiIdx = headerValue.lastIndexOf(";")
const datePart = headerValue.substring(semiIdx + 1)
this.date = Date.parse(datePart)
}
this.names = Received.parseValue(headerValue)
}
/**
* Parses the Received header name-value-list into a dictionary.
* @param headerValue {string} The full header value for the Received header
* @returns {{[string]: string}}
*/
static parseValue(headerValue: string): Record {
const dict: Record = {}
const semiIdx = headerValue.lastIndexOf(";")
headerValue = headerValue.replace(/\s+/, " ")
const headerValueWithoutDate = semiIdx > -1 ? headerValue.substring(0, semiIdx) : headerValue
// The regex below should capture the following:
// The name consists of non-whitespace characters followed by a whitespace and then the value follows.
// There are multiple cases for the value part:
// 1: Value is just some characters not including any whitespace
// 2: Value is some characters, a whitespace followed by an unlimited number of
// parenthesized values which can contain whitespaces, each delimited by whitespace
//
// Cheat sheet for regex:
// \s means every whitespace character
// [^\s] means every character except whitespace characters
// +? is a non-greedy equivalent of +
//const string pattern = @"(?[^\s]+)\s(?[^\s]+(\s\(.+?\))*)";
const pattern = /(?[^\s]+)\s(?[^\s]+(\s\(.+?\))*)/
// Find each match in the string
const matches = headerValueWithoutDate.matchAll(pattern)
for (let match of matches) {
// Add the name and value part found in the matched result to the dictionary
if (match.groups == null) throw new Error("unreachable!")
const name = match.groups["name"]
const value = match.groups["value"]
// Check if the name is really a comment.
// In this case, the first entry in the header value
// is a comment
if (name.startsWith("(")) continue
// Only add the first name pair
// All subsequent pairs are ignored, as they are invalid anyway
if (dict[name] != null) continue
dict[name] = value
}
return dict
}
}