Skip to content

Instantly share code, notes, and snippets.

@clouedoc
Created November 20, 2022 22:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save clouedoc/b59cdbc6bd38f3eb06b4df6ae4180acb to your computer and use it in GitHub Desktop.
Save clouedoc/b59cdbc6bd38f3eb06b4df6ae4180acb to your computer and use it in GitHub Desktop.
import type { IMailCredentials } from './credentials';
import { findAndFetchMessage } from './fetch-mail';
/**
* Check if the credentials are working.
* @param credentials
* @throws if the credentials are invalid
*/
export async function mailCheckValid(credentials: IMailCredentials): Promise<void> {
const messages = await findAndFetchMessage(credentials, ['ALL']);
if (messages.length < 1) {
throw new Error('There are no messages in the mailbox.');
}
}
/**
* Credentials to an email account.
*/
export interface IMailCredentials {
mail: string;
password: string;
}
const messages = await findAndFetchMessage(
{
mail: credentials.address,
password: credentials.password
},
['ALL']
);
import delay from 'delay';
import Imap from 'imap';
import { logger } from '../telemetry';
import type { IMailCredentials } from './credentials';
import type { IMail } from './mail';
import { findMailProvider, providerConfigurationMap, type IMailProviderConfig } from './provider';
/**
* Execute the given function with the given mail client
* @param credentials
* @param cb
* @returns
*/
async function executeWithMailClient<T>(
credentials: IMailCredentials,
cb: (imap: Imap) => T
): Promise<T> {
const mailProviderConfiguration: IMailProviderConfig =
providerConfigurationMap[findMailProvider(credentials.mail)];
const imap: Imap = new Imap({
host: mailProviderConfiguration.host,
port: mailProviderConfiguration.port,
user: credentials.mail,
password: credentials.password,
tls: true
});
logger.silly('Instantiated IMAP client', { credentials });
await Promise.all([
Promise.race([
new Promise<void>((_, reject) => {
imap.once('error', (err: Error) => {
reject(err);
});
}),
new Promise<void>((resolve) => imap.once('ready', resolve))
]),
imap.connect()
]);
logger.silly('Connected IMAP client', { credentials });
const output: T = await cb(imap);
imap.end();
return output;
}
/**
* Fetch messages matching the given search criteria
* @param credentials the credentials of the mail account
* @returns all the messages of the account
*/
export async function findAndFetchMessage(
credentials: IMailCredentials,
searchCriteria: string[] = ['ALL']
): Promise<IMail[]> {
return executeWithMailClient(credentials, async (imap) => {
return new Promise<IMail[]>((resolve, reject) => {
imap.openBox('INBOX', true, (err, box) => {
if (err) {
reject(err);
return;
}
imap.search([searchCriteria], async (err, results) => {
if (err) {
reject(err);
return;
}
let f: Imap.ImapFetch;
try {
f = imap.fetch(results, {
bodies: ''
});
} catch (err) {
reject(err);
return;
}
const messages: IMail[] = [];
f.on('message', (msg) => {
msg.on('body', async (stream) => {
const chunks: Buffer[] = [];
stream.on('data', (chunk) => {
chunks.push(chunk);
});
await new Promise<void>((resolve) => stream.on('end', resolve));
const message: string = Buffer.concat(chunks).toString().replace(/\r/g, '');
const [, headers, body] = /(.*?)\n\n(.*)/s.exec(message) ?? [];
if (!headers || !body) {
reject(new Error('Could not parse message'));
return;
}
messages.push({
headers: headers
.replace(/\n\t/g, '')
.split('\n')
.map((line) => line.split(':'))
.map((line) => [line[0]?.trim(), line[1]?.trim()])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
body
});
});
});
await delay(2000);
resolve(messages);
});
});
});
});
}
export type { IMailCredentials } from './credentials';
export { findAndFetchMessage } from './fetch-mail';
export type { IMail } from './mail';
export interface IMail {
headers: Record<string, string>;
body: string;
}
import { throwExpression } from '../utils';
enum MailProvider {
Outlook = 'outlook'
}
const domainToProviderMapping: Record<string, MailProvider> = {
'outlook.com': MailProvider.Outlook
};
export interface IMailProviderConfig {
host: string;
port: number;
}
export const providerConfigurationMap: Record<MailProvider, IMailProviderConfig> = {
[MailProvider.Outlook]: {
host: 'outlook.office365.com',
port: 993
}
};
/**
* Find the mail provider of a given email
*/
export function findMailProvider(mail: string): MailProvider {
const domain: string = mail.split('@')[1];
return (
domainToProviderMapping[domain] ?? throwExpression('Unknown mail provider for domain ' + domain)
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment