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;
32 FLAG_FROZEN_EXPIRATION,
37 const AUTOREPLY_HEADERS = ['X-Autoreply', 'X-Autorespond', 'X-Autoreply-From', 'X-Mail-Autoreply'];
38 const LIST_HEADERS = [
49 * Check if a message has a mime type
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);
58 * Check if a message has a flag in the flags bitmap
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 || '')
136 export const getPublicRecipients = (message?: Partial<Message>) => {
137 const { ToList = [], CCList = [] } = message || {};
138 return [...ToList, ...CCList];
142 * Get date from message
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)) {
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));
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 })) {
192 if (isAuto({ Flags })) {
196 const autoReplyHeaderValues = [
197 ['Auto-Submitted', 'auto-replied'],
198 ['Precedence', 'auto_reply'],
199 ['X-Precedence', 'auto_reply'],
200 ['Delivered-To', 'autoresponder'],
202 // These headers are not always available. But we should check them to support
203 // outlook / mail autoresponses.
205 AUTOREPLY_HEADERS.some((header) => header in ParsedHeaders) ||
206 autoReplyHeaderValues.some(([header, searchedValue]) =>
207 getParsedHeadersAsArray({ ParsedHeaders }, header)?.some((foundValue) => foundValue === searchedValue)
212 * Format the subject to add the prefix only when the subject
213 * doesn't start with it
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);