Skip to content

Commit a4c5375

Browse files
committed
fix: email replyto's
1 parent fbf8369 commit a4c5375

File tree

2 files changed

+109
-82
lines changed

2 files changed

+109
-82
lines changed
Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,62 @@
1-
import { GoogleAuth } from 'google-auth-library';
2-
import { prisma } from '../../prisma';
3-
import { EmailQueue } from '../types/email';
1+
import { GoogleAuth } from "google-auth-library";
2+
import { prisma } from "../../prisma";
3+
import { EmailQueue } from "../types/email";
44

55
export class AuthService {
6-
public static generateXOAuth2Token(username: string, accessToken: string): string {
6+
public static generateXOAuth2Token(
7+
username: string,
8+
accessToken: string
9+
): string {
710
const authString = [
811
`user=${username}`,
912
`auth=Bearer ${accessToken}`,
10-
'',
11-
''
12-
].join('\x01');
13-
return Buffer.from(authString).toString('base64');
13+
"",
14+
"",
15+
].join("\x01");
16+
return Buffer.from(authString).toString("base64");
1417
}
1518

1619
static async getValidAccessToken(queue: EmailQueue): Promise<string> {
17-
const { clientId, clientSecret, refreshToken, accessToken, expiresIn } = queue;
20+
const { clientId, clientSecret, refreshToken, accessToken, expiresIn } =
21+
queue;
1822

23+
// Check if token is still valid
1924
const now = Math.floor(Date.now() / 1000);
2025
if (accessToken && expiresIn && now < expiresIn) {
2126
return accessToken;
2227
}
2328

29+
// Initialize GoogleAuth client
2430
const auth = new GoogleAuth({
25-
clientOptions: { clientId, clientSecret }
31+
clientOptions: {
32+
clientId: clientId,
33+
clientSecret: clientSecret,
34+
},
2635
});
2736

2837
const oauth2Client = auth.fromJSON({
2938
client_id: clientId,
3039
client_secret: clientSecret,
31-
refresh_token: refreshToken
40+
refresh_token: refreshToken,
3241
});
3342

43+
// Refresh the token if expired
3444
const tokenInfo = await oauth2Client.getAccessToken();
35-
if (!tokenInfo.token) {
36-
throw new Error('Unable to refresh access token.');
37-
}
3845

39-
const expiryDate = (expiresIn || 0) + 3600;
40-
await prisma.emailQueue.update({
41-
where: { id: queue.id },
42-
data: {
43-
accessToken: tokenInfo.token,
44-
expiresIn: expiryDate
45-
}
46-
});
46+
const expiryDate = expiresIn! + 3600;
4747

48-
return tokenInfo.token;
48+
if (tokenInfo.token) {
49+
await prisma.emailQueue.update({
50+
where: { id: queue.id },
51+
data: {
52+
accessToken: tokenInfo.token,
53+
expiresIn: expiryDate,
54+
},
55+
});
56+
57+
return tokenInfo.token;
58+
} else {
59+
throw new Error("Unable to refresh access token.");
60+
}
4961
}
50-
}
62+
}
Lines changed: 73 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,64 @@
1-
import EmailReplyParser from 'email-reply-parser';
2-
import Imap from 'imap';
3-
import { simpleParser } from 'mailparser';
4-
import { prisma } from '../../prisma';
5-
import { EmailConfig, EmailQueue } from '../types/email';
6-
import { AuthService } from './auth.service';
1+
import EmailReplyParser from "email-reply-parser";
2+
import Imap from "imap";
3+
import { simpleParser } from "mailparser";
4+
import { prisma } from "../../prisma";
5+
import { EmailConfig, EmailQueue } from "../types/email";
6+
import { AuthService } from "./auth.service";
77

88
function getReplyText(email: any): string {
9-
const parsed = new EmailReplyParser().read(email.text);
10-
let replyText = ''
9+
const parsed = new EmailReplyParser().read(email.text);
10+
const fragments = parsed.getFragments();
1111

12-
parsed.fragments.forEach(fragment => {
13-
if (fragment.isHidden() && !fragment.isSignature() && !fragment.isQuoted()) return;
14-
replyText += fragment.content;
15-
});
12+
let replyText = "";
1613

17-
return replyText;
18-
14+
fragments.forEach((fragment: any) => {
15+
console.log("FRAGMENT", fragment._content, fragment.content);
16+
if (!fragment._isHidden && !fragment._isSignature && !fragment._isQuoted) {
17+
replyText += fragment._content;
18+
}
19+
});
20+
21+
return replyText;
1922
}
2023

2124
export class ImapService {
2225
private static async getImapConfig(queue: EmailQueue): Promise<EmailConfig> {
2326
switch (queue.serviceType) {
24-
case 'gmail': {
25-
const validatedAccessToken = await AuthService.getValidAccessToken(queue);
27+
case "gmail": {
28+
const validatedAccessToken = await AuthService.getValidAccessToken(
29+
queue
30+
);
31+
2632
return {
2733
user: queue.username,
2834
host: queue.hostname,
2935
port: 993,
3036
tls: true,
31-
xoauth2: AuthService.generateXOAuth2Token(queue.username, validatedAccessToken),
32-
tlsOptions: { rejectUnauthorized: false, servername: queue.hostname }
37+
xoauth2: AuthService.generateXOAuth2Token(
38+
queue.username,
39+
validatedAccessToken
40+
),
41+
tlsOptions: { rejectUnauthorized: false, servername: queue.hostname },
3342
};
3443
}
35-
case 'other':
44+
case "other":
3645
return {
3746
user: queue.username,
3847
password: queue.password,
3948
host: queue.hostname,
4049
port: queue.tls ? 993 : 143,
4150
tls: queue.tls || false,
42-
tlsOptions: { rejectUnauthorized: false, servername: queue.hostname }
51+
tlsOptions: { rejectUnauthorized: false, servername: queue.hostname },
4352
};
4453
default:
45-
throw new Error('Unsupported service type');
54+
throw new Error("Unsupported service type");
4655
}
4756
}
4857

49-
private static async processEmail(parsed: any, isReply: boolean): Promise<void> {
58+
private static async processEmail(
59+
parsed: any,
60+
isReply: boolean
61+
): Promise<void> {
5062
const { from, subject, text, html, textAsHtml } = parsed;
5163

5264
if (isReply) {
@@ -57,111 +69,114 @@ export class ImapService {
5769

5870
const ticketId = ticketIdMatch[1];
5971
const ticket = await prisma.ticket.findFirst({
60-
where: { Number: Number(ticketId) }
72+
where: { Number: Number(ticketId) },
6173
});
6274

6375
if (!ticket) {
6476
throw new Error(`Ticket not found: ${ticketId}`);
6577
}
6678

6779
const replyText = getReplyText(parsed);
80+
6881
await prisma.comment.create({
6982
data: {
70-
text: text ? replyText : 'No Body',
83+
text: text ? replyText : "No Body",
7184
userId: null,
7285
ticketId: ticket.id,
7386
reply: true,
7487
replyEmail: from.value[0].address,
75-
public: true
76-
}
88+
public: true,
89+
},
7790
});
7891
} else {
7992
const imapEmail = await prisma.imap_Email.create({
8093
data: {
8194
from: from.value[0].address,
82-
subject: subject || 'No Subject',
83-
body: text || 'No Body',
84-
html: html || '',
85-
text: textAsHtml
86-
}
95+
subject: subject || "No Subject",
96+
body: text || "No Body",
97+
html: html || "",
98+
text: textAsHtml,
99+
},
87100
});
88101

89102
await prisma.ticket.create({
90103
data: {
91104
email: from.value[0].address,
92105
name: from.value[0].name,
93-
title: imapEmail.subject || '-',
106+
title: imapEmail.subject || "-",
94107
isComplete: false,
95-
priority: 'Low',
108+
priority: "Low",
96109
fromImap: true,
97-
detail: html || textAsHtml
98-
}
110+
detail: html || textAsHtml,
111+
},
99112
});
100113
}
101114
}
102115

103116
static async fetchEmails(): Promise<void> {
104-
const queues = (await prisma.emailQueue.findMany()) as unknown as EmailQueue[];
117+
const queues =
118+
(await prisma.emailQueue.findMany()) as unknown as EmailQueue[];
105119
const today = new Date();
106120

107121
for (const queue of queues) {
108122
try {
109123
const imapConfig = await this.getImapConfig(queue);
110-
if (!imapConfig.password) {
111-
console.error('IMAP configuration is missing a password');
112-
throw new Error('IMAP configuration is missing a password');
124+
125+
if (queue.serviceType === "other" && !imapConfig.password) {
126+
console.error("IMAP configuration is missing a password");
127+
throw new Error("IMAP configuration is missing a password");
113128
}
114129

115130
// @ts-ignore
116131
const imap = new Imap(imapConfig);
117132

118133
await new Promise((resolve, reject) => {
119-
imap.once('ready', () => {
120-
imap.openBox('INBOX', false, (err) => {
134+
imap.once("ready", () => {
135+
imap.openBox("INBOX", false, (err) => {
121136
if (err) {
122137
reject(err);
123138
return;
124139
}
125-
imap.search(['UNSEEN', ['ON', today]], (err, results) => {
140+
imap.search(["UNSEEN", ["ON", today]], (err, results) => {
126141
if (err) reject(err);
127142
if (!results?.length) {
128-
console.log('No new messages');
143+
console.log("No new messages");
129144
imap.end();
130145
resolve(null);
131146
return;
132147
}
133148

134-
const fetch = imap.fetch(results, { bodies: '' });
135-
136-
fetch.on('message', (msg) => {
137-
msg.on('body', (stream) => {
149+
const fetch = imap.fetch(results, { bodies: "" });
150+
151+
fetch.on("message", (msg) => {
152+
msg.on("body", (stream) => {
138153
simpleParser(stream, async (err, parsed) => {
139154
if (err) throw err;
140-
const isReply = parsed.subject?.includes('Re:');
155+
const isReply = parsed.subject?.includes("Re:");
141156
await this.processEmail(parsed, isReply || false);
142157
});
143158
});
144159

145-
msg.once('attributes', (attrs) => {
146-
imap.addFlags(attrs.uid, ['\\Seen'], () => {
147-
console.log('Marked as read!');
160+
msg.once("attributes", (attrs) => {
161+
imap.addFlags(attrs.uid, ["\\Seen"], () => {
162+
console.log("Marked as read!");
148163
});
149164
});
150165
});
151166

152-
fetch.once('error', reject);
153-
fetch.once('end', () => {
154-
console.log('Done fetching messages');
167+
fetch.once("error", reject);
168+
fetch.once("end", () => {
169+
console.log("Done fetching messages");
155170
imap.end();
156171
resolve(null);
157172
});
158173
});
159174
});
160175
});
161176

162-
imap.once('error', reject);
163-
imap.once('end', () => {
164-
console.log('Connection ended');
177+
imap.once("error", reject);
178+
imap.once("end", () => {
179+
console.log("Connection ended");
165180
resolve(null);
166181
});
167182

@@ -172,4 +187,4 @@ export class ImapService {
172187
}
173188
}
174189
}
175-
}
190+
}

0 commit comments

Comments
 (0)