Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/classes/SMTPServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,15 @@ export class SMTPServer
splitter.on('data', (data: any)=>{
if(data.type === 'node')
{
// Inject from header if needed
try {
if(!data.headers.hasHeader('From') && envelope.mailFrom)
data.headers.add('From', envelope.mailFrom.address);
} catch(error) {
log('error', `Failed to inject from header`, {error});
}

// Inject bcc header if needed
try {
if(!data.headers.hasHeader('Bcc')) // We don't have a BCC header?
{
Expand Down
21 changes: 20 additions & 1 deletion test/02send/01basic.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { expect } from 'chai';
import { Message } from '@microsoft/microsoft-graph-types';
import Mail from 'nodemailer/lib/mailer';
import { config } from '../_config';
import { Server } from '../classes/Server';
import { submitAndVerifyMail } from './Helpers';
import LowLevelSMTPClient from '../classes/LowLevelSMTPClient';
import { defaultTransportOptions } from '../01receive/Helpers';
import { defaultMail, submitAndVerifyMail, verifyMail, waitForMessage } from './Helpers';

describe('Send: Basic', async function(){
const server = new Server({
Expand Down Expand Up @@ -79,4 +83,19 @@ describe('Send: Basic', async function(){
});
});

it('No From/To/Cc headers', async function(){
const client = new LowLevelSMTPClient(defaultTransportOptions.host!, defaultTransportOptions.port!);

const mail: Mail.Options = {
...defaultMail,
subject: `TEST: ${this.test?.title}`,
};

const messageId: string = await expect(client.sendMail(defaultMail.from as string, defaultMail.to as string, {Subject: mail.subject!}, defaultMail.text as string), 'Failed to send message').to.eventually.be.fulfilled;
const received: Message = await expect(waitForMessage(messageId), 'No message was send').to.eventually.be.fulfilled;

delete mail.to; // We don't expect a To header
await verifyMail(mail, messageId, received);
});

});
8 changes: 4 additions & 4 deletions test/02send/Helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const defaultMail: Mail.Options = {
export async function submitAndVerifyMail(submitProps: ISubmitMail)
{
const { props, send, received } = await submitMail(submitProps);
await verifyMail(props.mail!, send, received);
await verifyMail(props.mail!, send.messageId, received);

return {props, send, received};
}
Expand All @@ -40,9 +40,9 @@ export async function submitMail(props: ISubmitMail)
return {props, send, received};
}

export async function verifyMail(mail: Required<ISubmitMail>['mail'], send: SMTPTransport.SentMessageInfo, received: Message)
export async function verifyMail(mail: Required<ISubmitMail>['mail'], messageId: string, received: Message)
{
expect(received.internetMessageId, 'Message-Id from queued file doesn\'t match submitted message').to.equal(send.messageId);
expect(received.internetMessageId, 'Message-Id from queued file doesn\'t match submitted message').to.equal(messageId);

// Addresses
if(mail.from) expect(received.from?.emailAddress?.address, 'From address not present').to.equal(mail.from);
Expand Down Expand Up @@ -86,7 +86,7 @@ export async function verifyMail(mail: Required<ISubmitMail>['mail'], send: SMTP
}
}

async function waitForMessage(msgId: string)
export async function waitForMessage(msgId: string)
{
for(let i=0; i<15; i++)
{
Expand Down
64 changes: 64 additions & 0 deletions test/classes/LowLevelSMTPClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { createConnection, Socket } from 'node:net';
import { randomUUID } from 'node:crypto';

export class LowLevelSMTPClient
{
constructor(public server: string, public port: number)
{

}

async sendMail(from: string, rcptTo: string, headers: Record<string, string>, data: string): Promise<string>
{
// Add a messageId
headers['Message-ID'] ??= randomUUID().toString();

const socket = await this.#connect();
await this.#read(socket); // Wait for hello from server
await this.#writeRead(socket, `HELO ${this.server}\r\n`);
await this.#writeRead(socket, `MAIL FROM: <${from}>\r\n`);
await this.#writeRead(socket, `RCPT TO: <${rcptTo}>\r\n`);
await this.#writeRead(socket, `DATA\r\n`);
for(const [key, val] of Object.entries(headers))
socket.write(`${key}: ${val}\r\n`);
await this.#writeRead(socket, `\r\n${data}\r\n.\r\n`);
socket.destroy();

return headers['Message-ID'];
}

#connect(): Promise<Socket>
{
return new Promise<Socket>((resolve, reject)=>{
const socket = createConnection(this.port, this.server);
socket.on('error', reject);
socket.once('connect', ()=>{
socket.off('connect', reject);
resolve(socket);
});
});
}

async #writeRead(socket: Socket, data: string): Promise<string>
{
socket.write(data);
const response = (await this.#read(socket, 2000)).toString();

if(/^[23]\d{2} /.test(response)) // We got a 200/300 response?
return response;
else // No 200/300 response, then we throw an error
throw new Error(response);
}

#read(socket: Socket, timeout: number = 5000): Promise<Buffer>
{
const dataPromise = new Promise<Buffer>((resolve, reject)=>{
socket.once('data', resolve);
});
const dataTimeout = new Promise<Buffer>((resolve, reject)=>setTimeout(()=>reject(`Time-out. No data after ${timeout}ms`), timeout))

return Promise.race([dataPromise, dataTimeout]);
}
}

export default LowLevelSMTPClient;