Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / shared / lib / mail / messages.ts
blob17616b8e573c4fb2c21088643921e3c58c42f1ba
1 import { c } from 'ttag';
3 import identity from '@proton/utils/identity';
5 import { MAILBOX_LABEL_IDS, MIME_TYPES } from '../constants';
6 import { clearBit, hasBit, hasBitBigInt, setBit, toggleBit } from '../helpers/bitset';
7 import { canonicalizeInternalEmail, getEmailParts } from '../helpers/email';
8 import { isICS } from '../helpers/mimetype';
9 import type { AttachmentInfo, Message } from '../interfaces/mail/Message';
10 import { MESSAGE_FLAGS, SIGNATURE_START } from './constants';
12 const { PLAINTEXT, MIME } = MIME_TYPES;
13 const {
14     FLAG_RECEIVED,
15     FLAG_SENT,
16     FLAG_RECEIPT_REQUEST,
17     FLAG_RECEIPT_SENT,
18     FLAG_IMPORTED,
19     FLAG_REPLIED,
20     FLAG_REPLIEDALL,
21     FLAG_FORWARDED,
22     FLAG_INTERNAL,
23     FLAG_AUTO,
24     FLAG_E2E,
25     FLAG_SIGN,
26     FLAG_PUBLIC_KEY,
27     FLAG_UNSUBSCRIBED,
28     FLAG_SCHEDULED_SEND,
29     FLAG_DMARC_FAIL,
30     FLAG_PHISHING_AUTO,
31     FLAG_HAM_MANUAL,
32     FLAG_FROZEN_EXPIRATION,
33     FLAG_SUSPICIOUS,
34     FLAG_AUTO_FORWARDEE,
35     FLAG_AUTO_FORWARDER,
36 } = MESSAGE_FLAGS;
37 const AUTOREPLY_HEADERS = ['X-Autoreply', 'X-Autorespond', 'X-Autoreply-From', 'X-Mail-Autoreply'];
38 const LIST_HEADERS = [
39     'List-Id',
40     'List-Unsubscribe',
41     'List-Subscribe',
42     'List-Post',
43     'List-Help',
44     'List-Owner',
45     'List-Archive',
48 /**
49  * Check if a message has a mime type
50  */
51 export const hasMimeType = (type: MIME_TYPES) => (message?: Partial<Message>) => message?.MIMEType === type;
53 export const isMIME = hasMimeType(MIME);
54 export const isPlainText = hasMimeType(PLAINTEXT);
55 export const isHTML = hasMimeType(MIME_TYPES.DEFAULT);
57 /**
58  * Check if a message has a flag in the flags bitmap
59  */
60 export const hasFlag = (flag: number) => (message?: Partial<Message>) => hasBit(message?.Flags, flag);
61 export const hasBigFlag = (flag: bigint) => (message?: Partial<Message>) =>
62     hasBitBigInt(BigInt(message?.Flags || 0), flag);
63 export const setFlag = (flag: number) => (message?: Partial<Message>) => setBit(message?.Flags, flag);
64 export const clearFlag = (flag: number) => (message?: Partial<Message>) => clearBit(message?.Flags, flag);
65 export const toggleFlag = (flag: number) => (message?: Partial<Message>) => toggleBit(message?.Flags, flag);
67 export const isRequestReadReceipt = hasFlag(FLAG_RECEIPT_REQUEST);
68 export const isReadReceiptSent = hasFlag(FLAG_RECEIPT_SENT);
69 export const isImported = hasFlag(FLAG_IMPORTED);
70 export const isInternal = hasFlag(FLAG_INTERNAL);
71 export const isExternal = (message?: Partial<Message>) => !isInternal(message);
72 export const isAuto = hasFlag(FLAG_AUTO);
73 export const isReceived = hasFlag(FLAG_RECEIVED);
74 export const isSent = hasFlag(FLAG_SENT);
75 export const isReplied = hasFlag(FLAG_REPLIED);
76 export const isRepliedAll = hasFlag(FLAG_REPLIEDALL);
77 export const isForwarded = hasFlag(FLAG_FORWARDED);
78 export const isSentAndReceived = hasFlag(FLAG_SENT | FLAG_RECEIVED);
79 export const isDraft = (message?: Partial<Message>) =>
80     message?.Flags !== undefined && !isSent(message) && !isReceived(message);
81 export const isOutbox = (message?: Partial<Message>) => message?.LabelIDs?.includes(MAILBOX_LABEL_IDS.OUTBOX);
82 export const isExpiring = (message?: Partial<Message>) => !!message?.ExpirationTime;
83 export const isScheduled = (message?: Partial<Message>) => message?.LabelIDs?.includes(MAILBOX_LABEL_IDS.SCHEDULED);
84 export const isSnoozed = (message?: Partial<Message>) => message?.LabelIDs?.includes(MAILBOX_LABEL_IDS.SNOOZED);
85 export const isScheduledSend = hasFlag(FLAG_SCHEDULED_SEND);
86 export const isE2E = hasFlag(FLAG_E2E);
87 export const isSentEncrypted = hasFlag(FLAG_E2E | FLAG_SENT);
88 export const isInternalEncrypted = hasFlag(FLAG_E2E | FLAG_INTERNAL);
89 export const isSign = hasFlag(FLAG_SIGN);
90 export const isFrozenExpiration = hasBigFlag(FLAG_FROZEN_EXPIRATION);
91 export const isAttachPublicKey = hasFlag(FLAG_PUBLIC_KEY);
92 export const isUnsubscribed = hasFlag(FLAG_UNSUBSCRIBED);
93 export const isUnsubscribable = (message?: Partial<Message>) => {
94     const unsubscribeMethods = message?.UnsubscribeMethods || {};
95     return !!unsubscribeMethods.OneClick; // Only method supported by API
97 export const isDMARCValidationFailure = hasFlag(FLAG_DMARC_FAIL);
98 export const isAutoFlaggedPhishing = hasFlag(FLAG_PHISHING_AUTO);
99 export const isSuspicious = hasBigFlag(FLAG_SUSPICIOUS);
101 export const isManualFlaggedHam = hasFlag(FLAG_HAM_MANUAL);
102 export const isAutoForwarder = hasBigFlag(FLAG_AUTO_FORWARDER);
103 export const isAutoForwardee = hasBigFlag(FLAG_AUTO_FORWARDEE);
105 export const isExternalEncrypted = (message: Message) => isE2E(message) && !isInternal(message);
106 export const isPGPEncrypted = (message: Message) => isExternal(message) && isReceived(message) && isE2E(message);
107 export const inSigningPeriod = ({ Time = 0 }: Pick<Message, 'Time'>) =>
108     Time >= Math.max(SIGNATURE_START.USER, SIGNATURE_START.BULK);
109 export const isPGPInline = (message: Message) => isPGPEncrypted(message) && !isMIME(message);
110 export const isEO = (message?: Partial<Message>) => !!message?.Password;
111 export const addReceived = (Flags = 0) => setBit(Flags, MESSAGE_FLAGS.FLAG_RECEIVED);
113 export const isBounced = (message: Pick<Message, 'Sender' | 'Subject'>) => {
114     // we don't have a great way of determining when a message is bounced as the BE cannot offer us neither
115     // a specific header nor a specific flag. We hard-code the typical sender (the local part) and subject keywords
116     const { Sender, Subject } = message;
117     const matchesSender = getEmailParts(canonicalizeInternalEmail(Sender.Address))[0] === 'mailerdaemon';
118     const matchesSubject = [/delivery/i, /undelivered/i, /returned/i, /failure/i].some((regex) => regex.test(Subject));
119     return matchesSender && matchesSubject;
122 export const getSender = (message?: Pick<Message, 'Sender'>) => message?.Sender;
124 export const hasSimpleLoginSender = (message?: Pick<Message, 'Sender'>) => !!message?.Sender?.IsSimpleLogin;
125 export const hasProtonSender = (message?: Pick<Message, 'Sender'>) => !!message?.Sender?.IsProton;
127 export const getRecipients = (message?: Partial<Message>) => {
128     const { ToList = [], CCList = [], BCCList = [] } = message || {};
129     return [...ToList, ...CCList, ...BCCList];
131 export const getRecipientsAddresses = (message: Partial<Message>) =>
132     getRecipients(message)
133         .map(({ Address }) => Address || '')
134         .filter(identity);
136 export const getPublicRecipients = (message?: Partial<Message>) => {
137     const { ToList = [], CCList = [] } = message || {};
138     return [...ToList, ...CCList];
142  * Get date from message
143  */
144 export const getDate = ({ Time = 0 }: Message) => new Date(Time * 1000);
145 export const getParsedHeaders = (message: Partial<Message> | undefined, parameter: string) => {
146     const { ParsedHeaders = {} } = message || {};
147     return ParsedHeaders[parameter];
149 export const getParsedHeadersFirstValue = (message: Partial<Message> | undefined, parameter: string) => {
150     const value = getParsedHeaders(message, parameter);
151     return Array.isArray(value) ? value[0] : value;
153 export const getParsedHeadersAsArray = (message: Partial<Message> | undefined, parameter: string) => {
154     const value = getParsedHeaders(message, parameter);
155     return value === undefined ? undefined : Array.isArray(value) ? value : [value];
157 export const getOriginalTo = (message?: Partial<Message>) => {
158     return getParsedHeadersFirstValue(message, 'X-Original-To') || '';
160 export const requireReadReceipt = (message?: Partial<Message>) => {
161     const dispositionNotificationTo = getParsedHeaders(message, 'Disposition-Notification-To') || ''; // ex: Andy <andy@pm.me>
163     if (!dispositionNotificationTo || isSent(message)) {
164         return false;
165     }
167     return true;
169 export const getListUnsubscribe = (message?: Message) => getParsedHeadersAsArray(message, 'List-Unsubscribe');
170 export const getListUnsubscribePost = (message?: Message) => getParsedHeadersAsArray(message, 'List-Unsubscribe-Post');
171 export const getAttachments = (message?: Partial<Message>) => message?.Attachments || [];
172 export const hasAttachments = (message?: Partial<Message>) => !!(message?.NumAttachments && message.NumAttachments > 0);
173 export const attachmentsSize = (message?: Partial<Message>) =>
174     getAttachments(message).reduce((acc, { Size = 0 } = {}) => acc + +Size, 0);
175 export const getHasOnlyIcsAttachments = (attachmentInfo?: Partial<Record<MIME_TYPES, AttachmentInfo>>) => {
176     if (!!attachmentInfo) {
177         const keys = Object.keys(attachmentInfo);
178         return keys.length > 0 && !keys.some((key) => !isICS(key));
179     }
180     return false;
183 export const isAutoReply = (message?: Partial<Message>) => {
184     const ParsedHeaders = message?.ParsedHeaders || {};
185     return AUTOREPLY_HEADERS.some((h) => h in ParsedHeaders);
187 export const isSentAutoReply = ({ Flags, ParsedHeaders = {} }: Message) => {
188     if (!isSent({ Flags })) {
189         return false;
190     }
192     if (isAuto({ Flags })) {
193         return true;
194     }
196     const autoReplyHeaderValues = [
197         ['Auto-Submitted', 'auto-replied'],
198         ['Precedence', 'auto_reply'],
199         ['X-Precedence', 'auto_reply'],
200         ['Delivered-To', 'autoresponder'],
201     ];
202     // These headers are not always available. But we should check them to support
203     // outlook / mail autoresponses.
204     return (
205         AUTOREPLY_HEADERS.some((header) => header in ParsedHeaders) ||
206         autoReplyHeaderValues.some(([header, searchedValue]) =>
207             getParsedHeadersAsArray({ ParsedHeaders }, header)?.some((foundValue) => foundValue === searchedValue)
208         )
209     );
212  * Format the subject to add the prefix only when the subject
213  * doesn't start with it
214  */
215 export const formatSubject = (subject = '', prefix = '') => {
216     const hasPrefix = new RegExp(`^${prefix}`, 'i');
217     return hasPrefix.test(subject) ? subject : `${prefix} ${subject}`;
220 export const DRAFT_ID_PREFIX = 'draft';
221 export const ORIGINAL_MESSAGE = `------- Original Message -------`;
222 export const FORWARDED_MESSAGE = `------- Forwarded Message -------`;
223 export const RE_PREFIX = c('Message').t`Re:`;
224 export const FW_PREFIX = c('Message').t`Fw:`;
226 export const isNewsLetter = (message?: Pick<Message, 'ParsedHeaders'>) => {
227     const ParsedHeaders = message?.ParsedHeaders || {};
228     return LIST_HEADERS.some((h) => h in ParsedHeaders);