Skip to content

Commit 103fabf

Browse files
committed
chore: wip
1 parent 8a3ca4d commit 103fabf

File tree

5 files changed

+2382
-376
lines changed

5 files changed

+2382
-376
lines changed

config/email.ts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,185 @@ export default {
8989
cardDAV: false, // contacts sync
9090
activeSync: false, // Exchange ActiveSync
9191
},
92+
93+
/**
94+
* Automatic email categorization (Gmail-style folders)
95+
* Emails are automatically sorted into Social, Forums, Updates, Promotions folders
96+
* based on sender patterns. You can customize the patterns below.
97+
*/
98+
categorization: {
99+
enabled: true,
100+
101+
// Social networks - Facebook, Twitter, LinkedIn, etc.
102+
social: {
103+
domains: [
104+
'facebookmail.com', 'facebook.com', 'fb.com',
105+
'twitter.com', 'x.com',
106+
'linkedin.com', 'linkedinmail.com',
107+
'instagram.com',
108+
'pinterest.com',
109+
'snapchat.com',
110+
'tiktok.com',
111+
'reddit.com', 'redditmail.com',
112+
'tumblr.com',
113+
'whatsapp.com',
114+
'telegram.org',
115+
'discord.com', 'discordapp.com',
116+
'slack.com',
117+
'meetup.com',
118+
'nextdoor.com',
119+
'quora.com',
120+
'medium.com',
121+
'mastodon.social',
122+
'threads.net',
123+
'bluesky.social',
124+
],
125+
substrings: [
126+
'notification@', 'notifications@',
127+
'noreply@', 'no-reply@',
128+
'@social.', '@notifications.',
129+
'updates@', 'info@',
130+
],
131+
headers: {
132+
'x-mailer': ['facebook', 'twitter', 'linkedin'],
133+
},
134+
},
135+
136+
// Forums & mailing lists - Google Groups, Discourse, etc.
137+
forums: {
138+
domains: [
139+
'googlegroups.com', 'groups.google.com',
140+
'discourse.org',
141+
'stackoverflow.com', 'stackexchange.com',
142+
'freelancer.com',
143+
'upwork.com',
144+
'mailman.org',
145+
'listserv.net',
146+
'sympa.org',
147+
'yahoogroups.com',
148+
'topica.com',
149+
'gnu.org',
150+
'sourceforge.net',
151+
'launchpad.net',
152+
],
153+
substrings: [
154+
'-list@', '-users@', '-dev@', '-announce@',
155+
'forum@', 'discuss@', 'community@',
156+
'@lists.', '@mailman.', '@groups.',
157+
'reply+', // GitHub discussion replies
158+
],
159+
headers: {
160+
'list-unsubscribe': [''],
161+
'list-id': [''],
162+
'precedence': ['list', 'bulk'],
163+
'x-mailing-list': [''],
164+
},
165+
},
166+
167+
// Updates & transactional - GitHub, Stripe, shipping, etc.
168+
updates: {
169+
domains: [
170+
'github.com', 'gitlab.com', 'bitbucket.org',
171+
'stripe.com', 'paypal.com', 'square.com', 'venmo.com',
172+
'ups.com', 'fedex.com', 'usps.com', 'dhl.com',
173+
'amazon.com', 'amazonses.com',
174+
'google.com', 'accounts.google.com',
175+
'apple.com', 'id.apple.com',
176+
'microsoft.com', 'live.com', 'outlook.com',
177+
'dropbox.com', 'box.com',
178+
'atlassian.com', 'jira.com', 'trello.com',
179+
'notion.so', 'airtable.com', 'asana.com',
180+
'vercel.com', 'netlify.com', 'heroku.com',
181+
'cloudflare.com', 'digitalocean.com',
182+
'twilio.com', 'sendgrid.com',
183+
'intercom.io', 'zendesk.com', 'freshdesk.com',
184+
'calendly.com', 'cal.com',
185+
'zoom.us', 'zoom.com',
186+
'doordash.com', 'ubereats.com', 'grubhub.com',
187+
'airbnb.com', 'booking.com', 'expedia.com',
188+
'uber.com', 'lyft.com',
189+
'netflix.com', 'spotify.com', 'hulu.com',
190+
],
191+
substrings: [
192+
'alert@', 'alerts@',
193+
'notification@', 'notifications@',
194+
'noreply@', 'no-reply@',
195+
'security@', 'support@',
196+
'confirm@', 'confirmation@',
197+
'receipt@', 'invoice@', 'billing@',
198+
'shipping@', 'delivery@', 'order@', 'orders@',
199+
'account@', 'password@',
200+
'verify@', 'verification@',
201+
],
202+
headers: {
203+
'auto-submitted': ['auto-generated', 'auto-replied'],
204+
'x-auto-response-suppress': [''],
205+
},
206+
},
207+
208+
// Promotions & marketing - newsletters, sales, etc.
209+
promotions: {
210+
domains: [
211+
'mailchimp.com', 'mail.mailchimp.com',
212+
'sendgrid.net', 'sendgrid.com',
213+
'constantcontact.com',
214+
'mailerlite.com',
215+
'hubspot.com', 'hubspotmail.com',
216+
'klaviyo.com',
217+
'convertkit.com',
218+
'drip.com',
219+
'getresponse.com',
220+
'aweber.com',
221+
'campaignmonitor.com',
222+
'sendinblue.com', 'brevo.com',
223+
'activecampaign.com',
224+
'emarsys.net',
225+
'salesforce.com', 'exacttarget.com',
226+
'amazon.com', 'amazonsellerservices.com',
227+
'walmart.com',
228+
'target.com',
229+
'bestbuy.com',
230+
'ebay.com',
231+
'etsy.com',
232+
'shopify.com',
233+
'wish.com',
234+
'aliexpress.com',
235+
'wayfair.com',
236+
'homedepot.com',
237+
'lowes.com',
238+
'kohls.com',
239+
'macys.com',
240+
'nordstrom.com',
241+
'gap.com',
242+
'nike.com',
243+
'adidas.com',
244+
'lululemon.com',
245+
'uniqlo.com',
246+
'zara.com',
247+
'hm.com',
248+
'sephora.com',
249+
'ulta.com',
250+
'groupon.com',
251+
'retailmenot.com',
252+
],
253+
substrings: [
254+
'promo@', 'promotions@',
255+
'marketing@', 'newsletter@', 'news@',
256+
'deals@', 'offers@', 'sale@', 'sales@',
257+
'shop@', 'store@',
258+
'rewards@', 'loyalty@',
259+
'unsubscribe', // common in promotional emails
260+
'campaign', 'blast@',
261+
],
262+
headers: {
263+
'x-campaign': [''],
264+
'x-mailchimp-id': [''],
265+
'x-mc-user': [''],
266+
'x-sg-eid': [''], // SendGrid
267+
'x-ses-outgoing': [''], // AWS SES promotional
268+
},
269+
},
270+
},
92271
},
93272

94273
notifications: {

storage/framework/core/cloud/src/imap/client.ts

Lines changed: 92 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,21 +76,105 @@ export class AWSClient {
7676
}
7777

7878
/**
79-
* Load AWS credentials from environment variables
79+
* Load AWS credentials from environment variables or EC2 instance metadata
8080
*/
8181
private loadCredentials(): AWSCredentials {
8282
const accessKeyId = process.env.AWS_ACCESS_KEY_ID
8383
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY
8484
const sessionToken = process.env.AWS_SESSION_TOKEN
8585

86-
if (!accessKeyId || !secretAccessKey) {
87-
throw new Error('AWS credentials not found. Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables.')
86+
if (accessKeyId && secretAccessKey) {
87+
return {
88+
accessKeyId,
89+
secretAccessKey,
90+
sessionToken,
91+
}
8892
}
8993

94+
// Return placeholder - will be loaded async from EC2 metadata
95+
// The actual credentials will be fetched in getCredentials()
9096
return {
91-
accessKeyId,
92-
secretAccessKey,
93-
sessionToken,
97+
accessKeyId: '',
98+
secretAccessKey: '',
99+
}
100+
}
101+
102+
/**
103+
* Cache for EC2 instance metadata credentials
104+
*/
105+
private ec2CredentialsCache?: {
106+
credentials: AWSCredentials
107+
expiration: number
108+
}
109+
110+
/**
111+
* Get credentials, fetching from EC2 metadata if needed
112+
*/
113+
private async getCredentials(): Promise<AWSCredentials> {
114+
// Check environment variables first
115+
const accessKeyId = process.env.AWS_ACCESS_KEY_ID
116+
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY
117+
if (accessKeyId && secretAccessKey) {
118+
return {
119+
accessKeyId,
120+
secretAccessKey,
121+
sessionToken: process.env.AWS_SESSION_TOKEN,
122+
}
123+
}
124+
125+
// Check if we have cached EC2 credentials that haven't expired
126+
if (this.ec2CredentialsCache) {
127+
const now = Date.now()
128+
// Refresh 5 minutes before expiration
129+
if (this.ec2CredentialsCache.expiration > now + 5 * 60 * 1000) {
130+
return this.ec2CredentialsCache.credentials
131+
}
132+
}
133+
134+
// Fetch from EC2 instance metadata service (IMDSv2)
135+
try {
136+
// Get token for IMDSv2
137+
const tokenResponse = await fetch('http://169.254.169.254/latest/api/token', {
138+
method: 'PUT',
139+
headers: {
140+
'X-aws-ec2-metadata-token-ttl-seconds': '21600',
141+
},
142+
})
143+
const token = await tokenResponse.text()
144+
145+
// Get IAM role name
146+
const roleResponse = await fetch('http://169.254.169.254/latest/meta-data/iam/security-credentials/', {
147+
headers: { 'X-aws-ec2-metadata-token': token },
148+
})
149+
const roleName = await roleResponse.text()
150+
151+
// Get credentials for the role
152+
const credsResponse = await fetch(`http://169.254.169.254/latest/meta-data/iam/security-credentials/${roleName}`, {
153+
headers: { 'X-aws-ec2-metadata-token': token },
154+
})
155+
const credsData = await credsResponse.json() as {
156+
AccessKeyId: string
157+
SecretAccessKey: string
158+
Token: string
159+
Expiration: string
160+
}
161+
162+
const credentials: AWSCredentials = {
163+
accessKeyId: credsData.AccessKeyId,
164+
secretAccessKey: credsData.SecretAccessKey,
165+
sessionToken: credsData.Token,
166+
}
167+
168+
// Cache the credentials
169+
this.ec2CredentialsCache = {
170+
credentials,
171+
expiration: new Date(credsData.Expiration).getTime(),
172+
}
173+
174+
return credentials
175+
}
176+
catch (error) {
177+
throw new Error('AWS credentials not found. Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables, or run on an EC2 instance with an IAM role.')
94178
}
95179
}
96180

@@ -153,8 +237,8 @@ export class AWSClient {
153237
* Make the actual HTTP request
154238
*/
155239
private async makeRequest(options: AWSRequestOptions): Promise<any> {
156-
const credentials = options.credentials || this.credentials
157-
if (!credentials) {
240+
const credentials = options.credentials || await this.getCredentials()
241+
if (!credentials || !credentials.accessKeyId || !credentials.secretAccessKey) {
158242
throw new Error('AWS credentials not provided')
159243
}
160244

0 commit comments

Comments
 (0)