2f173dc3e5
CI / backend (push) Successful in 1m33s
CI / frontend (push) Successful in 33s
CI / format (push) Successful in 2m2s
CI / db-tests (push) Successful in 1m45s
Deploy Staging / deploy (push) Successful in 38s
Security / secrets (push) Successful in 6s
Security / dependencies (push) Successful in 1m17s
Security / sast (push) Successful in 56s
CI / backend (pull_request) Successful in 1m9s
CI / frontend (pull_request) Successful in 21s
CI / format (pull_request) Successful in 1m4s
CI / db-tests (pull_request) Successful in 1m27s
Security / secrets (pull_request) Successful in 6s
Security / dependencies (pull_request) Successful in 1m21s
Security / sast (pull_request) Successful in 1m8s
167 lines
7.9 KiB
JavaScript
167 lines
7.9 KiB
JavaScript
import axios from 'axios';
|
|
|
|
// Cookie-based auth (Google OAuth2 session), so send credentials with each call.
|
|
const api = axios.create({ baseURL: '/api/v1', withCredentials: true });
|
|
|
|
api.interceptors.response.use(
|
|
(r) => r,
|
|
(err) => {
|
|
if (err.response?.status === 401) {
|
|
// Not signed in. Send the user to the public landing page (unless already
|
|
// there) so they can read the features and choose to log in.
|
|
if (window.location.pathname !== '/') window.location.href = '/';
|
|
}
|
|
return Promise.reject(err);
|
|
}
|
|
);
|
|
|
|
// Top-level navigation that starts the Google flow and returns to the app.
|
|
export const LOGIN_URL = '/api/v1/auth/login?returnUrl=/app';
|
|
|
|
export const AppApi = {
|
|
info: () => api.get('/app/info').then((r) => r.data)
|
|
};
|
|
|
|
export const AuthApi = {
|
|
me: () => api.get('/auth/me').then((r) => r.data),
|
|
logout: () => api.post('/auth/logout')
|
|
};
|
|
|
|
export const SyncApi = {
|
|
status: () => api.get('/sync/status').then((r) => r.data),
|
|
full: () => api.post('/sync/full'),
|
|
incremental: () => api.post('/sync/incremental')
|
|
};
|
|
|
|
export const AnalyticsApi = {
|
|
dashboard: () => api.get('/analytics/dashboard').then((r) => r.data),
|
|
health: () => api.get('/analytics/health').then((r) => r.data),
|
|
topSenders: (take = 20) => api.get(`/analytics/top-senders?take=${take}`).then((r) => r.data),
|
|
volume: (days = 90) => api.get(`/analytics/volume?days=${days}`).then((r) => r.data),
|
|
categoryHeatmap: () => api.get('/analytics/category-heatmap').then((r) => r.data),
|
|
attachments: () => api.get('/analytics/attachments').then((r) => r.data),
|
|
sidebarCounts: () => api.get('/analytics/sidebar-counts').then((r) => r.data),
|
|
// Large take to retrieve all senders for the volume-tiers page.
|
|
allSenders: () => api.get('/analytics/top-senders?take=5000').then((r) => r.data)
|
|
};
|
|
|
|
export const CleanupApi = {
|
|
preview: (req) => api.post('/cleanup/preview', req).then((r) => r.data),
|
|
execute: (req) => api.post('/cleanup/execute', req).then((r) => r.data)
|
|
};
|
|
|
|
// Bulk actions over an explicit set of email IDs (selection-driven, always confirmed —
|
|
// the user already opted in by selecting rows and clicking the action).
|
|
// Numeric values must match CleanupActionType in Domain/Enums/Enums.cs (no string
|
|
// enum converter is configured on the API, so plain numbers are required here).
|
|
const CLEANUP_ACTION = { Archive: 0, Trash: 1, MarkRead: 5, MarkUnread: 6, Star: 7, Unstar: 8 };
|
|
|
|
export const BulkApi = {
|
|
markRead: (ids) => CleanupApi.execute({ action: CLEANUP_ACTION.MarkRead, emailIds: ids, confirmed: true }),
|
|
markUnread: (ids) => CleanupApi.execute({ action: CLEANUP_ACTION.MarkUnread, emailIds: ids, confirmed: true }),
|
|
star: (ids) => CleanupApi.execute({ action: CLEANUP_ACTION.Star, emailIds: ids, confirmed: true }),
|
|
unstar: (ids) => CleanupApi.execute({ action: CLEANUP_ACTION.Unstar, emailIds: ids, confirmed: true }),
|
|
archive: (ids) => CleanupApi.execute({ action: CLEANUP_ACTION.Archive, emailIds: ids, confirmed: true }),
|
|
trash: (ids) => CleanupApi.execute({ action: CLEANUP_ACTION.Trash, emailIds: ids, confirmed: true }),
|
|
};
|
|
|
|
export const UnsubscribeApi = {
|
|
detect: () => api.post('/unsubscribe/detect'),
|
|
safeList: () => api.get('/unsubscribe/safe-list').then((r) => r.data),
|
|
process: (req) => api.post('/unsubscribe/process', req).then((r) => r.data)
|
|
};
|
|
|
|
export const LayoutApi = {
|
|
get: () => api.get('/widgetlayout').then((r) => r.data),
|
|
save: (layout) => api.put('/widgetlayout', layout)
|
|
};
|
|
|
|
export const SearchApi = {
|
|
folder: (slug, page = 1, pageSize = 50) =>
|
|
api.post('/search', folderToRequest(slug, page, pageSize)).then((r) => r.data),
|
|
query: (q, page = 1, pageSize = 50) =>
|
|
api.get('/search', { params: { q, page, pageSize } }).then((r) => r.data),
|
|
};
|
|
|
|
function folderToRequest(slug, page, pageSize) {
|
|
const base = { page, pageSize };
|
|
switch (slug) {
|
|
// ── Mailbox ──
|
|
case 'inbox': return { ...base, isInInbox: true, isTrashed: false };
|
|
case 'allmail': return { ...base };
|
|
case 'unread': return { ...base, isUnread: true };
|
|
case 'starred': return { ...base, isStarred: true };
|
|
case 'sent': return { ...base, gmailLabel: 'SENT' };
|
|
case 'drafts': return { ...base, gmailLabel: 'DRAFT' };
|
|
// Archive = filed away: not in inbox, not trashed, and NOT Sent/Spam/Draft/Chat.
|
|
// Excluding those labels stops sent mail leaking into Archive.
|
|
case 'archive': return { ...base, isInInbox: false, isTrashed: false, excludeGmailLabels: ['SENT', 'DRAFT', 'SPAM', 'TRASH', 'CHAT'] };
|
|
case 'spam': return { ...base, gmailLabel: 'SPAM' };
|
|
case 'trash': return { ...base, isTrashed: true };
|
|
// Pinned = Gmail's "Important" marker (a real per-message flag), not "everything".
|
|
case 'pinned': return { ...base, isImportant: true };
|
|
// Read Later = only emails the user explicitly flagged (local marker).
|
|
case 'readlater': return { ...base, isReadLater: true };
|
|
// Unlabelled = emails not filed under any user-created label.
|
|
case 'unlabelled': return { ...base, hasUserLabels: false };
|
|
// ── Special filters ──
|
|
case 'large': return { ...base, minSizeBytes: 5_000_000 };
|
|
// Old Mail = strictly older than 6 months.
|
|
case 'old': {
|
|
const d = new Date(); d.setMonth(d.getMonth() - 6);
|
|
return { ...base, to: d.toISOString().slice(0, 10) };
|
|
}
|
|
// ── Smart folders ──
|
|
case 'automated': return { ...base, category: 'Notification' };
|
|
case 'finance': return { ...base, category: 'Finance' };
|
|
case 'social': return { ...base, category: 'Social' };
|
|
case 'shopping': return { ...base, category: 'Shopping' };
|
|
case 'noreply': return { ...base, query: 'from:noreply' };
|
|
case 'gaming': return { ...base, category: 'Gaming' };
|
|
case 'sales': return { ...base, category: 'SeasonalSales' };
|
|
case 'ridesharing': return { ...base, category: 'RideSharing' };
|
|
case 'food': return { ...base, category: 'FoodDelivery' };
|
|
case 'wellness': return { ...base, category: 'Wellness' };
|
|
// New categories
|
|
case 'travel': return { ...base, category: 'Travel' };
|
|
case 'subscriptions': return { ...base, category: 'Subscriptions' };
|
|
case 'parcels': return { ...base, category: 'Parcels' };
|
|
case 'recruitment': return { ...base, category: 'Recruitment' };
|
|
case 'events': return { ...base, category: 'Events' };
|
|
case 'security': return { ...base, category: 'SecurityAlerts' };
|
|
case 'healthcare': return { ...base, category: 'Healthcare' };
|
|
case 'education': return { ...base, category: 'Education' };
|
|
case 'news': return { ...base, category: 'NewsMedia' };
|
|
case 'property': return { ...base, category: 'PropertyUtilities' };
|
|
case 'charity': return { ...base, category: 'Charity' };
|
|
case 'government': return { ...base, category: 'Government' };
|
|
case 'crypto': return { ...base, category: 'CryptoInvesting' };
|
|
case 'family': return { ...base, category: 'FamilySchool' };
|
|
default: return { ...base };
|
|
}
|
|
}
|
|
|
|
export const EmailApi = {
|
|
get: (id) => api.get(`/email/${id}`).then((r) => r.data),
|
|
summary: (id) => api.get(`/email/${id}/summary`).then((r) => r.data),
|
|
markRead: (id) => api.post(`/email/${id}/read`),
|
|
markUnread: (id) => api.post(`/email/${id}/unread`),
|
|
star: (id) => api.post(`/email/${id}/star`),
|
|
unstar: (id) => api.post(`/email/${id}/unstar`),
|
|
trash: (id) => api.post(`/email/${id}/trash`),
|
|
untrash: (id) => api.post(`/email/${id}/untrash`),
|
|
unsubscribe:(id) => api.post(`/email/${id}/unsubscribe`).then((r) => r.data),
|
|
};
|
|
|
|
export const ExportApi = {
|
|
reportUrl: (format) => `/api/v1/export/report?format=${format}`
|
|
};
|
|
|
|
export const SettingsApi = {
|
|
getDigest: () => api.get('/settings/digest').then((r) => r.data),
|
|
setDigest: (enabled) => api.put('/settings/digest', enabled).then((r) => r.data),
|
|
sendDigestNow: () => api.post('/settings/digest/send-now'),
|
|
};
|
|
|
|
export default api;
|