Skip to content

Commit

Permalink
feat: add helper for email recipients (#41)
Browse files Browse the repository at this point in the history
* feat: add email recipients handler

* feat: add spec for email

* chore: bump version

* docs: add js docs

* chore: lint

* feat: update node in CI

* feat: remove os matrix

* feat: update node

* fix: publish node version

* fix: os

* chore: update checkout version
  • Loading branch information
scmmishra authored Feb 6, 2025
1 parent 967998a commit 0bd2242
Show file tree
Hide file tree
Showing 7 changed files with 478 additions and 8 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
node: ['16.x']
os: [ubuntu-latest, windows-latest, macOS-latest]
node: ['16.x', '20.x', '23.x']
os: ['ubuntu-latest']

steps:
- name: Checkout repo
uses: actions/checkout@v2
uses: actions/checkout@v4

- name: Use Node ${{ matrix.node }}
uses: actions/setup-node@v1
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '16.x'
node-version: '22.x'
registry-url: 'https://registry.npmjs.org'
scope: '@chatwoot'
- run: yarn
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@chatwoot/utils",
"version": "0.0.35",
"version": "0.0.36",
"description": "Chatwoot utils",
"private": false,
"license": "MIT",
Expand Down
89 changes: 89 additions & 0 deletions src/email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { EmailMessage, MessageType } from './types/message';

/**
* Determines the recipients for an email reply based on the last email message's details,
* the conversation contact, and system-specific email addresses.
*/
export function getRecipients(
lastEmail: EmailMessage,
conversationContact: string,
inboxEmail: string,
forwardToEmail: string
) {
let to = [] as string[];
let cc = [] as string[];
let bcc = [] as string[];

// Reset emails if there's no lastEmail
if (!lastEmail) {
return { to, cc, bcc };
}

// Extract values from lastEmail and current conversation context
const {
content_attributes: { email: emailAttributes },
message_type: messageType,
} = lastEmail;

let isLastEmailFromContact = false;
if (emailAttributes) {
// this will be false anyway if the last email was outgoing
isLastEmailFromContact = (emailAttributes.from ?? []).includes(
conversationContact
);

if (messageType === MessageType.INCOMING) {
// Reply to sender if incoming
to.push(...(emailAttributes.from ?? []));
} else {
// Otherwise, reply to the last recipient (for outgoing message)
to.push(...(emailAttributes.to ?? []));
}

// Start building the cc list, including additional recipients
// If the email had multiple recipients, include them in the cc list
cc = emailAttributes.cc ? [...emailAttributes.cc] : [];
if (Array.isArray(emailAttributes.to)) {
cc.push(...emailAttributes.to);
}

// Add the conversation contact to cc if the last email wasn't sent by them
if (!isLastEmailFromContact) {
cc.push(conversationContact);
}

// Process BCC: Remove conversation contact from bcc as it is already in cc
bcc = (emailAttributes.bcc || []).filter(
emailAddress => emailAddress !== conversationContact
);
}

// Filter out undesired emails from cc:
// - Remove conversation contact from cc if they sent the last email
// - Remove inbox and forward-to email to prevent loops
// - Remove emails matching the reply UUID pattern
const replyUUIDPattern = /^reply\+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/i;
cc = cc.filter(email => {
if (email === conversationContact && isLastEmailFromContact) {
return false;
}
if (email === inboxEmail || email === forwardToEmail) {
return false;
}
if (replyUUIDPattern.test(email)) {
return false;
}
return true;
});

// Deduplicate each recipient list by converting to a Set then back to an array
to = Array.from(new Set(to));
cc = Array.from(new Set(cc));
bcc = Array.from(new Set(bcc));

return {
to,
cc,
bcc,
};
}
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
getFileInfo,
} from './helpers';

import { getRecipients } from './email';

import { parseBoolean } from './string';
import { sortAsc, quantile, clamp, getQuantileIntervals } from './math';
import {
Expand Down Expand Up @@ -44,4 +46,5 @@ export {
trimContent,
downloadFile,
getFileInfo,
getRecipients,
};
99 changes: 99 additions & 0 deletions src/types/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
export type EmailAttributes = {
bcc: string[] | null;
cc: string[] | null;
content_type: string;
date: string;
from: string[] | null;
html_content: {
full: string;
reply: string;
quoted: string;
};
in_reply_to: null;
message_id: string;
multipart: boolean;
number_of_attachments: number;
subject: string;
text_content: {
full: string;
reply: string;
quoted: string;
};
to: string[] | null;
};

export type MessageContentAttribute = {
cc_emails: string[] | null;
bcc_emails: string[] | null;
to_emails: string[] | null;
email: EmailAttributes | null;
external_error: string;
};

export type MessageConversation = {
id: number;
assignee_id: number;
custom_attributes: Record<string, any>;
first_reply_created_at: number;
waiting_since: number;
status: string;
unread_count: number;
last_activity_at: number;
contact_inbox: { source_id: string };
};

export type MessageAttachment = {
id: number;
message_id: number;
file_type: string;
account_id: number;
extension: null;
data_url: string;
thumb_url: string;
file_size: number;
width: null;
height: null;
};

export type MessageSender = {
custom_attributes: {};
email: null;
id: number;
identifier: null;
name: string;
phone_number: null;
thumbnail: string;
type: string;
};

export enum MessageType {
INCOMING = 0,
OUTGOING = 1,
ACTIVITY = 2,
TEMPLATE = 3,
}

export type EmailMessage = {
id: number;
content: null;
account_id: number;
inbox_id: number;
conversation_id: number;
message_type: MessageType;
created_at: number;
updated_at: string;
private: boolean;
status: string;
source_id: null;
content_type: string;
content_attributes: MessageContentAttribute;
sender_type: string;
sender_id: number;
external_source_ids: {};
additional_attributes: {};
processed_message_content: null;
sentiment: {};
conversation: MessageConversation;
attachments: MessageAttachment[];
sender: MessageSender;
};
Loading

0 comments on commit 0bd2242

Please sign in to comment.