Skip to content

Commit

Permalink
Merge pull request #25 from fterh/info-command
Browse files Browse the repository at this point in the history
Support alias info command
  • Loading branch information
fterh authored Feb 10, 2020
2 parents a65ee58 + fc0eb2c commit 25bae18
Show file tree
Hide file tree
Showing 27 changed files with 945 additions and 439 deletions.
24 changes: 21 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Changelog can be found under Releases.
2. **Reply anonymously:** Reply to emails from your alias without revealing your personal email address.
3. **Attachments:** Attachments are supported on incoming and outgoing emails (subject to size limits - see below).
4. **Email commands:** Manage your aliases through email directly - no separate app or website required.
5. **Usage stats:** Easily check the usage stats of each alias.

### Receiving emails

Expand Down Expand Up @@ -78,6 +79,19 @@ Dev note: This reads up to a maximum of 1MB of data (due to AWS's limitations).

Email `[email protected]` with the alias as the title (case-sensitive). You will receive the operation outcome (success/failure) as a reply.

### Usage stats

Email `[email protected]` with the alias as the title (case-sensitive).
You will receive usage information for the particular alias.

Supported usage stats:

- Alias creation date
- Emails received
- Emails sent
- Date of last received email
- Date of last sent email

#### Update an alias

Coming soon - not supported yet.
Expand Down Expand Up @@ -109,9 +123,13 @@ If you want to build new features or tweak existing features, you can set up a p

1. Ensure that the `DEV_SUBDOMAIN` environment variable is set in `.env` (e.g. `test`).
2. Run `yarn run deploy-dev`.
This creates a parallel development CloudFormation stack.
This creates a parallel development CloudFormation stack.
3. Add a new receipt rule in SES **before your production rule** to trigger your development S3 bucket.
For "recipients", enter the same test subdomain as you set in step 1 (e.g. `test.yourverifieddomain.com`).
Preferably, name your rule descriptively (e.g. `dev`).
For "recipients", enter the same test subdomain as you set in step 1 (e.g. `test.yourverifieddomain.com`).
Preferably, name your rule descriptively (e.g. `dev`).

Note: You need to update your DNS records for `test.yourverifieddomain.com` as you did when verifying your domain for AWS SES.

## Migration

To run migration scripts, first compile using `tsc scripts/migrate_vX.ts`, then run using `node scripts/migrate_vX.js`.
1 change: 0 additions & 1 deletion lib/__mocks__/generateAlias.ts

This file was deleted.

19 changes: 0 additions & 19 deletions lib/aliasExists.ts

This file was deleted.

3 changes: 2 additions & 1 deletion lib/commandSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
* Maintains a collection of reserved command keywords.
*/

export default new Set(["generate", "list", "remove"]);
export default new Set(["generate", "info", "list", "remove"]);

export enum Commands {
Generate = "generate",
Info = "info",
List = "list",
Remove = "remove"
}
24 changes: 4 additions & 20 deletions lib/commands/generate.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,17 @@
import { DynamoDB } from "aws-sdk";
import { ParsedMail } from "mailparser";
import aliasExists from "../aliasExists";
import { email, operationalDomain } from "../env";
import { Commands } from "../commandSet";
import generateAlias from "../generateAlias";
import Alias from "../models/Alias";
import sendEmail from "../sendEmail";
import storeAliasDescriptionRecord from "../storeAliasDescriptionRecord";

export default async (parsedMail: ParsedMail): Promise<void> => {
const docClient = new DynamoDB.DocumentClient(); // Avoid re-initializing

const description = parsedMail.subject;

let generatedAlias: string;
do {
generatedAlias = generateAlias();
} while (await aliasExists(generatedAlias, docClient));
console.log(
`Generated alias=${generatedAlias} for description=${description}`
);

console.log("Attempting to store alias-description record");
storeAliasDescriptionRecord(generatedAlias, description, docClient);
console.log("Successfully stored alias-description record");
const alias = await Alias.generateAlias(description);

await sendEmail({
from: `${Commands.Generate}@${operationalDomain}`,
to: [email],
subject: `Generated alias: ${generatedAlias}`,
text: `You have generated ${generatedAlias}@${operationalDomain} for "${description}".`
subject: `Generated alias: ${alias.value}`,
text: `You have generated ${alias.value}@${operationalDomain} for "${alias.description}".`
});
};
7 changes: 7 additions & 0 deletions lib/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ParsedMail } from "mailparser";

import { email } from "../env";
import generate from "./generate";
import info from "./info";
import list from "./list";
import remove from "./remove";
import { Commands } from "../commandSet";
Expand All @@ -19,12 +20,18 @@ export default async (command: string, parsedMail: ParsedMail) => {
await generate(parsedMail);
break;

case Commands.Info:
console.log("Invoking info command");
await info(parsedMail);
break;

case Commands.List:
console.log("Invoking list command");
await list();
break;

case Commands.Remove:
console.log("Invoking remove command");
await remove(parsedMail);
break;

Expand Down
49 changes: 49 additions & 0 deletions lib/commands/info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { ParsedMail } from "mailparser";
import { Commands } from "../commandSet";
import { email, operationalDomain } from "../env";
import Alias from "../models/Alias";
import sendEmail from "../sendEmail";

const commandEmailAddress = `${Commands.Info}@${operationalDomain}`;

export const prepareAliasInfoText = (alias: Alias): string => {
let res = "";

res += `Alias: ${alias.value}@${operationalDomain}\n`;
res += `Description: ${alias.description}\n`;
res += `Created: ${alias.creationDate ? alias.creationDate : "Unknown"}\n`;
res += `Emails received: ${alias.countReceived}\n`;
res += `Emails sent: ${alias.countSent}\n`;
res += `Email last received on: ${
alias.lastReceivedDate ? alias.lastReceivedDate : "-"
}\n`;
res += `Email last sent on: ${
alias.lastSentDate ? alias.lastSentDate : "-"
}\n\n`;

res += `Information generated on ${new Date()}`;

return res;
};

export default async (parsedMail: ParsedMail): Promise<void> => {
const aliasValue = parsedMail.subject;
const alias = await Alias.getAlias(aliasValue);

if (alias === undefined) {
await sendEmail({
from: commandEmailAddress,
to: email,
subject: `Info: ${aliasValue}@${operationalDomain} does not exist`,
text: "-"
});
return;
}

await sendEmail({
from: commandEmailAddress,
to: email,
subject: `Info: ${aliasValue}@${operationalDomain}`,
text: prepareAliasInfoText(alias)
});
};
50 changes: 10 additions & 40 deletions lib/commands/list.ts
Original file line number Diff line number Diff line change
@@ -1,65 +1,35 @@
import { DynamoDB } from "aws-sdk";
import config from "../config";
import { email, operationalDomain } from "../env";
import { Commands } from "../commandSet";
import Alias from "../models/Alias";
import sendEmail from "../sendEmail";

export default async (): Promise<void> => {
const docClient = new DynamoDB.DocumentClient();
const docParams: DynamoDB.DocumentClient.ScanInput = {
TableName: config.tableName
};
const getAliasesResults = await Alias.getAllAliases();

const records: DynamoDB.DocumentClient.ScanOutput = await docClient
.scan(docParams)
.promise();

console.log("Successfully scanned database for records");

if (records.Items && records.Items.length === 0) {
return await sendEmail({
if (getAliasesResults.aliases.length === 0) {
await sendEmail({
from: `${Commands.List}@${operationalDomain}`,
to: [email],
subject: `Alias list (${new Date()})`,
subject: `Alias list (generated on: ${new Date()})`,
text: "No aliases found."
});
}

let mightHaveMoreRecords = false;
let hasMalformedRecords = false;

if (records.LastEvaluatedKey) {
mightHaveMoreRecords = true;
console.log(
"Scan results' LastEvaluatedKey is not undefined; there might be more records in the results set"
);
return;
}

let output = "Alias : Description\n";
records.Items?.forEach(item => {
if (item.description === undefined) {
hasMalformedRecords = true;
return console.log(
`Record with alias=${item.alias} is missing the "description" attribute. Skipping.`
);
}
output += `${item.alias} : ${item.description}\n`;
getAliasesResults.aliases.forEach(alias => {
output += `${alias.value} : ${alias.description}\n`;
});

if (mightHaveMoreRecords) {
if (getAliasesResults.lastEvaluatedKey !== undefined) {
output +=
"There might be more records in the results set. Check the logs and database for more information.";
}

if (hasMalformedRecords) {
output +=
"The database contains malformed records. Check the logs and database for more information.";
}

await sendEmail({
from: `${Commands.List}@${operationalDomain}`,
to: [email],
subject: `Alias list (${new Date()})`,
subject: `Alias list (generated on: ${new Date()})`,
text: output
});
};
24 changes: 16 additions & 8 deletions lib/forwardInbound.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ParsedMail } from "mailparser";
import Mail from "nodemailer/lib/mailer";
import { email as personalEmail, operationalDomain } from "./env";
import getAliasDescription from "./getAliasDescription";
import Alias from "./models/Alias";
import repackageReceivedAttachments from "./repackageReceivedAttachments";
import sendEmail from "./sendEmail";
import senderAddressEncodeDecode from "./senderAddressEncodeDecode";
Expand All @@ -13,7 +13,7 @@ import senderAddressEncodeDecode from "./senderAddressEncodeDecode";
* Prioritizes original email's "reply-to" header over "from" header.
*/
export const generateFromHeader = (
alias: string,
aliasValue: string,
parsedMail: ParsedMail
): Mail.Address => {
let replyToEmailAddress = "";
Expand All @@ -32,27 +32,27 @@ export const generateFromHeader = (
return {
name: `${replyToName} <${replyToEmailAddress}>`,
address: senderAddressEncodeDecode.encodeEmailAddress(
alias,
aliasValue,
replyToEmailAddress
)
};
};

export const generateInboundMailOptions = async (
alias: string,
alias: Alias,
parsedMail: ParsedMail
): Promise<Mail.Options> => {
const mailOptions: Mail.Options = {
from: generateFromHeader(alias, parsedMail),
from: generateFromHeader(alias.value, parsedMail),
to: parsedMail.to.value,
cc: parsedMail.cc?.value,
subject: `[${await getAliasDescription(alias)}] ${parsedMail.subject}`,
subject: parsedMail.subject,
html:
parsedMail.html !== false
? (parsedMail.html as string) // Will never be `true`
: parsedMail.textAsHtml,
envelope: {
from: `${alias}@${operationalDomain}`, // For semantics only; this has no significance
from: `${alias.value}@${operationalDomain}`, // For semantics only; this has no significance
to: personalEmail
},
attachments: repackageReceivedAttachments(parsedMail.attachments)
Expand All @@ -65,11 +65,19 @@ export const generateInboundMailOptions = async (
* Forwards received email to personal email.
* Preserves metadata while avoiding re-sending to other recipients.
*/
export default async (alias: string, parsedMail: ParsedMail): Promise<void> => {
export default async (
aliasValue: string,
parsedMail: ParsedMail
): Promise<void> => {
console.log("Attempting to forward received email to personal email");

const alias = await Alias.getAlias(aliasValue);
if (alias === undefined) {
throw new Error(`Alias=${aliasValue} not found in database!`);
}
const mailOptions = await generateInboundMailOptions(alias, parsedMail);
await sendEmail(mailOptions);
await alias.didReceiveEmail();

console.log("Successfully forwarded email to personal email");
};
Loading

0 comments on commit 25bae18

Please sign in to comment.