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
28 changes: 28 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,38 @@ OUTLOOK_CLIENT_ID=your-outlook-client-id
OUTLOOK_CLIENT_SECRET=your-outlook-client-secret
OUTLOOK_TENANT_ID=your-outlook-tenant-id
OUTLOOK_REDIRECT_URI=https://localhost:5002/auth/outlook/callback
# SSL paths: server looks for `SSL_KEY_FILE` / `SSL_CRT_FILE` by default.
# Older setups may use `SSL_KEY_PATH` / `SSL_CERT_PATH` — both are commonly used.
SSL_KEY_FILE=ssl/localhost-key.pem
SSL_CRT_FILE=ssl/localhost-cert.pem
SSL_KEY_PATH=ssl/localhost-key.pem
SSL_CERT_PATH=ssl/localhost-cert.pem

# Frontend/backend URLs and redirect configuration
REACT_APP_BACKEND_URL=https://localhost:5002
FRONTEND_REDIRECT_URL=http://localhost:5003/
FRONTEND_POSTAUTH_ROUTE=/home

# Optional/advanced env vars added during development
# CORS: comma-separated list of allowed origins (overrides default local list)
ALLOWED_ORIGINS=https://localhost:5003,http://localhost:3000
# Classifier service URL used by backend (POST /classify)
CLASSIFIER_URL=http://localhost:5001
# Backend host/port used as fallbacks/logging
BACKEND_HOST=localhost
BACKEND_PORT=5002

# Frontend options
REACT_APP_USE_GROUP_LOGO=false
REACT_APP_TITLE=ImfrisivMail – Organize your emails
REACT_APP_DESCRIPTION="A brief description of the app"
REACT_APP_KEYWORDS=ImfrisivMail,email,productivity

# Classification categories (comma-separated)
CLASSIFICATION_CATEGORIES=important,spam,newsletter,social,promotional,personal,business,automated

# Optional API keys used by features (DO NOT COMMIT real secrets to the repo)
OPENAI_API_KEY=your-openai-key
PERPLEXITY_API_KEY=your-perplexity-key
# FRONTEND_REDIRECT_URL is used as a fallback for dynamic OAuth2 redirects if not provided by the frontend.
# FRONTEND_POSTAUTH_ROUTE sets the route to redirect to after authentication (default: /home)
55 changes: 55 additions & 0 deletions .github/workflows/env-keys-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Env File Keys Check

on:
pull_request:
types: [opened, synchronize, reopened]

jobs:
check-env-keys:
runs-on: ubuntu-latest
name: Verify required env keys are listed in .env.example
steps:
- uses: actions/checkout@v4

- name: Verify env keys
run: |
set -euo pipefail
REQUIRED_KEYS=(
"GMAIL_CLIENT_ID"
"GMAIL_CLIENT_SECRET"
"GMAIL_REDIRECT_URI"
"OUTLOOK_CLIENT_ID"
"OUTLOOK_CLIENT_SECRET"
"OUTLOOK_TENANT_ID"
"OUTLOOK_REDIRECT_URI"
"REACT_APP_BACKEND_URL"
"FRONTEND_REDIRECT_URL"
"FRONTEND_POSTAUTH_ROUTE"
"SSL_KEY_FILE"
"SSL_CRT_FILE"
"SSL_KEY_PATH"
"SSL_CERT_PATH"
"ALLOWED_ORIGINS"
"CLASSIFIER_URL"
"BACKEND_PORT"
"BACKEND_HOST"
"REACT_APP_USE_GROUP_LOGO"
"CLASSIFICATION_CATEGORIES"
)

MISSING=()
for key in "${REQUIRED_KEYS[@]}"; do
if ! grep -q "^${key}=" .env.example; then
MISSING+=("$key")
fi
done

if [ ${#MISSING[@]} -ne 0 ]; then
echo "ERROR: The following required env keys are missing from .env.example:"
for k in "${MISSING[@]}"; do
echo " - $k"
done
echo "\nPlease add them to .env.example (use placeholders, do NOT commit secrets)."
exit 1
fi
echo "All required env keys present in .env.example"
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,21 @@ This was proposed as the SiFri-Mail project for the S–CSSE321 and S–CSIS311
# FRONTEND_POSTAUTH_ROUTE sets the route to redirect to after authentication (default: /home)
```

### Additional environment variables and defaults

- `ALLOWED_ORIGINS` — Optional comma-separated list of origins allowed by CORS (e.g. `https://localhost:5003,http://localhost:3000`).
- Default: a sensible local-dev set including `localhost` and `127.0.0.1` on ports 3000, 5000, 5002, 5003, 5173.
- `CLASSIFIER_URL` — URL to the classifier service used by the Gmail route (POST /classify).
- Default: `http://localhost:5001`
- `BACKEND_PORT` — Port to run the backend on (used when `PORT` is not set).
- Default: `5002`
- `BACKEND_HOST` — Hostname used in local HTTPS startup logs (not required for operation).
- Default: `localhost`

Notes:
- The backend will read `ALLOWED_ORIGINS` and split by comma if present; otherwise it uses the default local origins. This makes it easier to run the frontend on a different port or host in development without editing source files.
- `REACT_APP_BACKEND_URL` continues to control where the frontend sends auth/login requests. If unset, the frontend will default to `https://<current-host>:<BACKEND_PORT>` when running on `localhost`/`127.0.0.1`, or to the current page origin in non-local environments.

5. Train the email classifier (optional):
```bash
cd backend/classifier
Expand Down
5 changes: 3 additions & 2 deletions backend/routes/gmail.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,9 @@ router.get("/callback", async (req, res) => {
}
}
console.log("Email Bodies to Classify: ", emailBodies);
// Call classifier
const classificationResponse = await axios.post("http://localhost:5001/classify", {
// Call classifier (URL configurable via CLASSIFIER_URL env var)
const classifierBase = process.env.CLASSIFIER_URL || "http://localhost:5001";
const classificationResponse = await axios.post(`${classifierBase.replace(/\/$/,"")}/classify`, {
emails: emailBodies,
});
Comment on lines +76 to 79
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider validating CLASSIFIER_URL for trailing slashes.

Also, trim whitespace from CLASSIFIER_URL to prevent issues from accidental spaces or formatting.

Suggested change
const classifierBase = process.env.CLASSIFIER_URL || "http://localhost:5001";
const classificationResponse = await axios.post(`${classifierBase.replace(/\/$/,"")}/classify`, {
emails: emailBodies,
});
let classifierBase = process.env.CLASSIFIER_URL || "http://localhost:5001";
classifierBase = classifierBase.trim().replace(/\/+$/, "");
const classificationResponse = await axios.post(`${classifierBase}/classify`, {
emails: emailBodies,
});

const classifications = classificationResponse.data.predictions;
Expand Down
61 changes: 33 additions & 28 deletions backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,36 @@ const app = express(); // Create an Express application instance

app.use(bodyParser.json()); // Use body-parser to parse JSON request bodies

const allowedOrigins = [
// Localhost (HTTPS)
"https://localhost:3000",
"https://localhost:5002",
"https://localhost:5000",
"https://localhost:5003",
"https://localhost:5173",
// Localhost (HTTP)
"http://localhost:3000",
"http://localhost:5002",
"http://localhost:5000",
"http://localhost:5003",
"http://localhost:5173",
// 127.0.0.1 loopback (HTTPS)
"https://127.0.0.1:3000",
"https://127.0.0.1:5002",
"https://127.0.0.1:5000",
"https://127.0.0.1:5003",
"https://127.0.0.1:5173",
// 127.0.0.1 loopback (HTTP)
"http://127.0.0.1:3000",
"http://127.0.0.1:5002",
"http://127.0.0.1:5000",
"http://127.0.0.1:5003",
"http://127.0.0.1:5173",
];
// Allow configuring allowed CORS origins via environment variable `ALLOWED_ORIGINS` as a comma-separated list.
// If not provided, fall back to a safe default set used for local development.
const allowedOrigins = process.env.ALLOWED_ORIGINS
? process.env.ALLOWED_ORIGINS.split(",").map((s) => s.trim())
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider validating ALLOWED_ORIGINS input for empty strings.

Empty strings in allowedOrigins may cause unexpected CORS behavior. Filter them out after splitting to improve robustness.

Suggested change
? process.env.ALLOWED_ORIGINS.split(",").map((s) => s.trim())
? process.env.ALLOWED_ORIGINS.split(",").map((s) => s.trim()).filter(Boolean)

: [
// Localhost (HTTPS)
"https://localhost:3000",
"https://localhost:5002",
"https://localhost:5000",
"https://localhost:5003",
"https://localhost:5173",
// Localhost (HTTP)
"http://localhost:3000",
"http://localhost:5002",
"http://localhost:5000",
"http://localhost:5003",
"http://localhost:5173",
// 127.0.0.1 loopback (HTTPS)
"https://127.0.0.1:3000",
"https://127.0.0.1:5002",
"https://127.0.0.1:5000",
"https://127.0.0.1:5003",
"https://127.0.0.1:5173",
// 127.0.0.1 loopback (HTTP)
"http://127.0.0.1:3000",
"http://127.0.0.1:5002",
"http://127.0.0.1:5000",
"http://127.0.0.1:5003",
"http://127.0.0.1:5173",
];

app.use(
cors({
Expand Down Expand Up @@ -120,7 +124,7 @@ app.use((req, res) => {
});

// Start server in a way that's safe on Vercel (platform provides HTTPS)
const PORT = process.env.PORT || 5002; // Ensure backend uses port 5002
const PORT = process.env.PORT || process.env.BACKEND_PORT || 5002; // Ensure backend uses BACKEND_PORT or default 5002

// Detect Vercel or similar serverless hosting environment. Vercel exposes
// `VERCEL` or `VERCEL_ENV` / `NOW_REGION` environment variables at runtime.
Expand All @@ -144,7 +148,8 @@ if (isVercel) {
cert: fs.readFileSync(certPath),
};
https.createServer(options, app).listen(PORT, () => {
console.log(`HTTPS server running at https://localhost:${PORT}`);
const host = process.env.BACKEND_HOST || "localhost";
console.log(`HTTPS server running at https://${host}:${PORT}`);
Comment on lines 146 to +152
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: BACKEND_HOST may not match actual binding host.

If the server binds to a different host than BACKEND_HOST, the log output may be inaccurate. Consider clarifying the log message or ensuring the binding matches BACKEND_HOST.

Suggested change
https.createServer(options, app).listen(PORT, () => {
console.log(`HTTPS server running at https://localhost:${PORT}`);
const host = process.env.BACKEND_HOST || "localhost";
console.log(`HTTPS server running at https://${host}:${PORT}`);
https.createServer(options, app).listen(PORT, () => {
// The server binds to 0.0.0.0 or localhost by default unless specified otherwise.
// BACKEND_HOST may be used for external access, but may not match the actual binding host.
const externalHost = process.env.BACKEND_HOST || "localhost";
console.log(`HTTPS server running (bound to port ${PORT}). External access: https://${externalHost}:${PORT}`);

});
} catch (err) {
console.error('Failed to start HTTPS server, falling back to HTTP:', err);
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,13 @@ function App() {
}, []);

// Function to handle Gmail login via OAuth
const backendUrl = process.env.REACT_APP_BACKEND_URL || "https://localhost:5002";
// Backend URL is configurable via `REACT_APP_BACKEND_URL`. If not provided, in development we'll point
// to localhost on `BACKEND_PORT` (default 5002). In production default to the current origin.
const backendUrl =
process.env.REACT_APP_BACKEND_URL ||
((window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')
? `https://${window.location.hostname}:${process.env.BACKEND_PORT || 5002}`
: `${window.location.protocol}//${window.location.host}`);
Comment on lines +41 to +45
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Mixing protocol and port logic may cause issues in some dev setups.

Defaulting to https for localhost can cause connection errors if the backend uses http. Consider detecting the protocol or making it configurable to support different development setups.

Suggested change
const backendUrl =
process.env.REACT_APP_BACKEND_URL ||
((window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')
? `https://${window.location.hostname}:${process.env.BACKEND_PORT || 5002}`
: `${window.location.protocol}//${window.location.host}`);
// Allow protocol override for localhost via REACT_APP_BACKEND_PROTOCOL, default to 'http'
const backendProtocol =
process.env.REACT_APP_BACKEND_PROTOCOL ||
((window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') ? 'http' : window.location.protocol.replace(':', ''));
const backendUrl =
process.env.REACT_APP_BACKEND_URL ||
((window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')
? `${backendProtocol}://${window.location.hostname}:${process.env.BACKEND_PORT || 5002}`
: `${window.location.protocol}//${window.location.host}`);

const frontendUrl = window.location.origin;
const handleGmailLogin = () => {
window.location.href = `${backendUrl}/auth/gmail/login?redirect=${encodeURIComponent(frontendUrl)}`;
Expand Down