Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
019b8a4
feat: Add Dockerfile and docker-compose for containerization, update …
theepicsaxguy Jan 16, 2026
605ecb8
feat(logging): Implement structured logging and server initialization…
theepicsaxguy Jan 16, 2026
9a4e231
feat: Update Node.js dependencies and configuration for improved perf…
theepicsaxguy Jan 16, 2026
581ec72
feat: Add GitHub Actions workflow for Docker build and push
theepicsaxguy Jan 16, 2026
e62ff5e
feat: Update Docker metadata tags for improved versioning and release…
theepicsaxguy Jan 16, 2026
0caddce
feat: Update Docker build configuration to enable image pushing and r…
theepicsaxguy Jan 16, 2026
5f14e7e
feat: Add image labels for enhanced Docker metadata in build configur…
theepicsaxguy Jan 16, 2026
6119be2
feat: Implement JMAP session API and update routing configuration
theepicsaxguy Jan 16, 2026
757d12a
feat: Refactor JMAP session handling and enhance security headers in …
theepicsaxguy Jan 16, 2026
96dd2d3
feat: Add QEMU setup step for multi-platform Docker builds
theepicsaxguy Jan 16, 2026
9608982
feat: Add enhanced logging to JMAP session endpoint for debugging
theepicsaxguy Jan 16, 2026
d071036
fix: Handle 307 redirect and fix mixed content security issues
theepicsaxguy Jan 16, 2026
3f7a8e8
feat: Add comprehensive security hardening
theepicsaxguy Jan 16, 2026
b63045b
feat: Update JMAP session GET handler to follow redirects and enforce…
theepicsaxguy Jan 16, 2026
2e9b207
feat: Add caching for Docker layers to optimize build times
theepicsaxguy Jan 16, 2026
76de427
feat: Simplify Docker caching by removing redundant cache steps
theepicsaxguy Jan 16, 2026
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
15 changes: 15 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Dockerfile
.dockerignore
.git
.gitignore
README.md
ROADMAP.md
CONTRIBUTING.md
.env*.local
.next
.vercel
*.md
.github
.vscode
screenshots
node_modules
127 changes: 120 additions & 7 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,9 +1,122 @@
# JMAP Webmail Configuration
# Copy this file to .env.local and fill in your values
# ============================================================
# JMAP WEBMAIL - ENVIRONMENT CONFIGURATION
# ============================================================
# This file documents all environment variables that can be
# configured for Docker/Kubernetes deployments.
# Copy this file to .env.local for local development.
# ============================================================

# App name displayed in the UI
NEXT_PUBLIC_APP_NAME=JMAP Webmail
# ============================================================
# REQUIRED CONFIGURATION
# ============================================================

# JMAP server URL (required)
# This is the URL of your JMAP-compatible mail server
NEXT_PUBLIC_JMAP_SERVER_URL=https://your-jmap-server.com
# JMAP server URL (REQUIRED for email functionality)
# This should point to your Stalwart Mail Server or other JMAP-compatible server
JMAP_SERVER_URL=https://mail.example.com

# ============================================================
# APPLICATION SETTINGS
# ============================================================

# Application name displayed in the UI
# Default: Webmail
APP_NAME=JMAP Webmail

# ============================================================
# INTERNATIONALIZATION & LOCALIZATION
# ============================================================

# Server timezone for date/time rendering
# Default: UTC
# Examples: UTC, America/New_York, Europe/London, Europe/Paris, Asia/Tokyo
TZ=UTC

# Alternative timezone environment variable (same as TZ)
# TIMEZONE=UTC

# ============================================================
# HEALTH CHECK CONFIGURATION
# ============================================================

# Memory warning threshold (0.0 to 1.0)
# Triggers "degraded" status when heap usage exceeds this percentage
# Default: 0.85 (85%)
HEALTH_MEMORY_WARNING_THRESHOLD=0.85

# Memory critical threshold (0.0 to 1.0)
# Triggers "unhealthy" status and returns HTTP 503
# Default: 0.95 (95%)
HEALTH_MEMORY_CRITICAL_THRESHOLD=0.95

# ============================================================
# NODE.JS & NEXT.JS CONFIGURATION
# ============================================================

# Node environment
# Values: production, development
# Default: production
NODE_ENV=production

# Node.js memory allocation (in MB)
# Controls the maximum heap size for Node.js
# Default: Node.js default (~1.4GB on 64-bit, or auto-detected)
# Recommended for containers: 512MB minimum for Next.js
# Adjust based on your container resource limits
NODE_OPTIONS=--max-old-space-size=512

# Port the application listens on
# Default: 3000
# Note: Must match the container's exposed port
PORT=3000

# Hostname to bind to
# Default: 0.0.0.0 (all interfaces - required for containers)
# For local development, you might use: localhost or 127.0.0.1
HOSTNAME=0.0.0.0

# ============================================================
# LOGGING CONFIGURATION
# ============================================================

# Log level controls verbosity of logs
# Values: error, warn, info, debug
# Default: info
# - error: Only log errors
# - warn: Log warnings and errors
# - info: Log general information, warnings, and errors (recommended)
# - debug: Log everything including debug information
LOG_LEVEL=info

# Log format for output
# Values: text, json
# Default: text
# - text: Human-readable colored logs (good for development)
# - json: Structured JSON logs (good for production/log aggregation)
# Use 'json' with: Kubernetes, Fluentd, Loki, CloudWatch, Datadog, etc.
LOG_FORMAT=text

# ============================================================
# LEGACY SUPPORT (Build-time variables)
# ============================================================
# These are only used as fallbacks if runtime variables are not set
# Prefer using the runtime variables above for Docker/Kubernetes

# NEXT_PUBLIC_APP_NAME=Webmail
# NEXT_PUBLIC_JMAP_SERVER_URL=https://mail.example.com

# ============================================================
# KUBERNETES SECRETS EXAMPLE
# ============================================================
# In Kubernetes, you can reference secrets instead of hardcoding values:
#
# env:
# - name: JMAP_SERVER_URL
# valueFrom:
# secretKeyRef:
# name: stalwart-config
# key: jmap-url
# - name: APP_NAME
# value: "My Corporate Webmail"
# - name: TZ
# value: "America/New_York"
# ============================================================
123 changes: 123 additions & 0 deletions .github/workflows/docker-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
name: Docker Build & Push

on:
pull_request:
branches:
- main
paths:
- 'app/**'
- 'components/**'
- 'lib/**'
- 'public/**'
- 'Dockerfile'
- 'next.config.ts'
- 'tsconfig.json'
- 'package.json'
- 'package-lock.json'
- '.dockerignore'
release:
types: [published]

permissions:
contents: read
packages: write

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Extract metadata
id: meta
run: |
# Get version from package.json
VERSION=$(grep '"version"' package.json | head -1 | sed 's/.*"version": "\([^"]*\)".*/\1/')
echo "version=${VERSION}" >> $GITHUB_OUTPUT

# Determine if this is a release
if [[ "${{ github.event_name }}" == "release" ]]; then
echo "is_release=true" >> $GITHUB_OUTPUT
echo "release_tag=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT
else
echo "is_release=false" >> $GITHUB_OUTPUT
fi

echo "Metadata: version=${VERSION}, is_release=${{ github.event_name == 'release' }}"

- name: Docker meta
id: docker_meta
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/${{ github.repository }}
tags: |
type=ref,event=pr,prefix=pr-,suffix=
type=semver,pattern={{version}},value=${{ steps.meta.outputs.version }},enable=${{ steps.meta.outputs.is_release }}
type=semver,pattern={{major}}.{{minor}},value=${{ steps.meta.outputs.version }},enable=${{ steps.meta.outputs.is_release }}
type=semver,pattern={{major}},value=${{ steps.meta.outputs.version }},enable=${{ steps.meta.outputs.is_release }}
type=raw,value=latest,enable=${{ steps.meta.outputs.is_release }}
type=sha
labels: |
org.opencontainers.image.description=A modern, privacy-focused webmail client built with Next.js and the JMAP protocol.
org.opencontainers.image.source=https://github.com/root-fr/jmap-webmail
org.opencontainers.image.licenses=MIT

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push (pull request)
if: github.event_name == 'pull_request'
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64
push: true
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
sbom: true
provenance: true

- name: Build and push (release)
if: github.event_name == 'release'
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
sbom: true
provenance: true

notify:
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'release'
steps:
- name: Release notification
run: |
echo "✅ Docker image built and pushed to GHCR"
echo "📦 Repository: ghcr.io/${{ github.repository }}"
echo "🏷️ Tags:"
echo " - ${{ github.event.release.tag_name }}"
echo " - latest"
70 changes: 70 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Build stage
FROM node:22-alpine AS builder

WORKDIR /app

# Copy package files
COPY package*.json ./

# Update npm to pinned version
RUN npm install -g npm@11.7.0

# Install dependencies
RUN npm install --frozen-lockfile

# Copy source code
COPY . .

# Build the application
RUN npm run build

# Production stage
FROM node:22-alpine AS runner

WORKDIR /app

# Install curl for healthcheck
RUN apk add --no-cache curl

# Create a non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# Copy package files
COPY package*.json ./

# Update npm to pinned version
RUN npm install -g npm@11.7.0

# Install production dependencies
RUN npm install --omit=dev --ignore-scripts && npm cache clean --force

# Copy public assets
COPY --from=builder /app/public ./public

# Copy built application
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next

# Switch to non-root user
USER nextjs

# Expose port
EXPOSE 3000

# Set environment variables
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
ENV APP_NAME=Webmail

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/api/health || exit 1

# Labels for Kubernetes
LABEL org.opencontainers.image.title="JMAP Webmail"
LABEL org.opencontainers.image.description="A modern, privacy-focused webmail client built with Next.js and the JMAP protocol"
LABEL org.opencontainers.image.source="https://github.com/root-fr/jmap-webmail"
LABEL org.opencontainers.image.licenses="MIT"

# Start the application
CMD ["npm", "start"]
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ This webmail client is designed to work seamlessly with [**Stalwart Mail Server*

### Prerequisites

- Node.js 18+
- Node.js 22+
- A JMAP-compatible mail server (we recommend [Stalwart](https://stalw.art/))

### Installation
Expand Down
1 change: 1 addition & 0 deletions app/[locale]/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default function LoginPage() {
const [formData, setFormData] = useState({
username: "",
password: "",
totpCode: "",
});

const [savedUsernames, setSavedUsernames] = useState<string[]>([]);
Expand Down
Loading