Skip to content
Open
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
158 changes: 158 additions & 0 deletions workspace-server/src/__tests__/services/GmailService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,164 @@ describe('GmailService', () => {
expect(response.attachments).toEqual([]);
});

it('should extract Cc, Bcc and Reply-To headers when present', async () => {
const mockMessage = {
id: 'msg1',
threadId: 'thread1',
payload: {
headers: [
{ name: 'From', value: 'sender@example.com' },
{ name: 'To', value: 'recipient@example.com' },
{ name: 'Cc', value: 'cc-recipient@example.com' },
{ name: 'Bcc', value: 'archive@example.com' },
{ name: 'Reply-To', value: 'replies@example.com' },
{ name: 'Subject', value: 'Test Email' },
],
body: {
data: 'SGVsbG8gV29ybGQh', // Base64 for "Hello World!"
},
},
};

mockGmailAPI.users.messages.get.mockResolvedValue({
data: mockMessage,
});

const result = await gmailService.get({
messageId: 'msg1',
format: 'full',
});

const response = JSON.parse(result.content[0].text);
expect(response.cc).toBe('cc-recipient@example.com');
expect(response.bcc).toBe('archive@example.com');
expect(response.replyTo).toBe('replies@example.com');
});

it('should omit Cc, Bcc and Reply-To when absent', async () => {
const mockMessage = {
id: 'msg1',
threadId: 'thread1',
payload: {
headers: [
{ name: 'From', value: 'sender@example.com' },
{ name: 'To', value: 'recipient@example.com' },
{ name: 'Subject', value: 'Test Email' },
],
body: {
data: 'SGVsbG8gV29ybGQh', // Base64 for "Hello World!"
},
},
};

mockGmailAPI.users.messages.get.mockResolvedValue({
data: mockMessage,
});

const result = await gmailService.get({
messageId: 'msg1',
format: 'full',
});

const response = JSON.parse(result.content[0].text);
expect(response).not.toHaveProperty('cc');
expect(response).not.toHaveProperty('bcc');
expect(response).not.toHaveProperty('replyTo');
});

it('should omit headers whose value is null', async () => {
const mockMessage = {
id: 'msg1',
threadId: 'thread1',
payload: {
headers: [
{ name: 'From', value: 'sender@example.com' },
{ name: 'To', value: 'recipient@example.com' },
{ name: 'Cc', value: null },
{ name: 'Subject', value: 'Test Email' },
],
body: {
data: 'SGVsbG8gV29ybGQh', // Base64 for "Hello World!"
},
},
};

mockGmailAPI.users.messages.get.mockResolvedValue({
data: mockMessage,
});

const result = await gmailService.get({
messageId: 'msg1',
format: 'full',
});

const response = JSON.parse(result.content[0].text);
expect(response).not.toHaveProperty('cc');
});

it('should return the first value when duplicate headers exist', async () => {
const mockMessage = {
id: 'msg1',
threadId: 'thread1',
payload: {
headers: [
{ name: 'From', value: 'sender@example.com' },
{ name: 'To', value: 'recipient@example.com' },
{ name: 'Subject', value: 'First Subject' },
{ name: 'Subject', value: 'Second Subject' },
],
body: {
data: 'SGVsbG8gV29ybGQh', // Base64 for "Hello World!"
},
},
};

mockGmailAPI.users.messages.get.mockResolvedValue({
data: mockMessage,
});

const result = await gmailService.get({
messageId: 'msg1',
format: 'full',
});

const response = JSON.parse(result.content[0].text);
expect(response.subject).toBe('First Subject');
});

it('should match header names case-insensitively', async () => {
const mockMessage = {
id: 'msg1',
threadId: 'thread1',
payload: {
headers: [
{ name: 'from', value: 'sender@example.com' },
{ name: 'TO', value: 'recipient@example.com' },
{ name: 'CC', value: 'cc-recipient@example.com' },
{ name: 'subject', value: 'Test Email' },
],
body: {
data: 'SGVsbG8gV29ybGQh', // Base64 for "Hello World!"
},
},
};

mockGmailAPI.users.messages.get.mockResolvedValue({
data: mockMessage,
});

const result = await gmailService.get({
messageId: 'msg1',
format: 'full',
});

const response = JSON.parse(result.content[0].text);
expect(response.from).toBe('sender@example.com');
expect(response.to).toBe('recipient@example.com');
expect(response.cc).toBe('cc-recipient@example.com');
expect(response.subject).toBe('Test Email');
});

it('should extract attachments in full format', async () => {
const mockMessage = {
id: 'msg_with_attach',
Expand Down
19 changes: 17 additions & 2 deletions workspace-server/src/services/GmailService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,12 +198,24 @@ export class GmailService {
// Extract useful information based on format
if (format === 'metadata' || format === 'full') {
const headers = message.payload?.headers || [];
const getHeader = (name: string) =>
headers.find((h) => h.name === name)?.value;
// Header names are case-insensitive per RFC 5322.
const headerMap = new Map<string, string>();
for (const h of headers) {
if (h.name && h.value != null) {
const key = h.name.toLowerCase();
if (!headerMap.has(key)) {
headerMap.set(key, h.value);
}
}
}
Comment thread
mickey-mikey marked this conversation as resolved.
const getHeader = (name: string) => headerMap.get(name.toLowerCase());

const subject = getHeader('Subject');
const from = getHeader('From');
const to = getHeader('To');
const cc = getHeader('Cc');
const bcc = getHeader('Bcc');
const replyTo = getHeader('Reply-To');
const date = getHeader('Date');

// Extract body and attachments for full format
Expand All @@ -228,6 +240,9 @@ export class GmailService {
subject,
from,
to,
cc,
bcc,
replyTo,
date,
body: body || message.snippet,
attachments: attachments,
Expand Down
Loading