Add initial project structure with Docker support and configuration files

- Create .copilotignore and .dockerignore to exclude config files
- Add Dockerfile for building the application
- Introduce Makefile for Docker commands
- Create ReadMe.md with project description and usage instructions
- Add project.env for environment variables
- Implement example config.json and localization files (en.json, hu.json)
- Develop main application logic in main.js with IMAP monitoring and notification
- Define package.json with dependencies and scripts
This commit is contained in:
Krisztián Szabolcsi 2026-01-10 12:30:24 +01:00
commit b92ec64746
13 changed files with 431 additions and 0 deletions

1
.copilotignore Normal file
View File

@ -0,0 +1 @@
src/config.json

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
config.json

35
.gitignore vendored Normal file
View File

@ -0,0 +1,35 @@
# Node
node_modules/
.npm/
package-lock.json
# Build / output
dist/
# Runtime / environment
.env
.env.*.local
# VS Code
.vscode/
!.vscode/settings.json
!.vscode/extensions.json
!.vscode/launch.json
!.vscode/tasks.json
# IDEs / editors
.idea/
*.suo
*.user
*.userosscache
*.sln.docstates
# OS files
.DS_Store
Thumbs.db
# project files
config.json
# Keep .gitkeep files so empty folders can be tracked
!**/.gitkeep

18
Dockerfile Normal file
View File

@ -0,0 +1,18 @@
# Simple Node.js Dockerfile
FROM node:18-alpine
WORKDIR /usr/src/app
# Install dependencies (use package-lock.json if present)
COPY src ./
RUN npm install --only=production
# Use a non-root user for safety
RUN addgroup -S app && adduser -S -G app app
USER app
EXPOSE 3000
ENV NODE_ENV=production
# Start the app (change to ["npm","start"] if you use an npm script)
CMD ["node", "main.js"]

47
Makefile Normal file
View File

@ -0,0 +1,47 @@
# Makefile for ImapNotification
ROOT_DIR := $(shell pwd)
include $(ROOT_DIR)/project.env
export
PORT ?= 8080
.PHONY: all build run stop shell help
all: build
build:
@sudo docker build \
-t $(NAME):local \
.
update:
@sudo docker run \
--rm \
-it \
--name $(NAME) \
-v "$(ROOT_DIR)/src:/usr/src/app" \
$(NAME):local \
npm update
.
run:
@sudo docker run \
--rm \
-it \
--name $(NAME) \
--env-file $(ROOT_DIR)/.env \
-v "$(ROOT_DIR)/src:/usr/src/app" \
$(NAME):local
stop:
@sudo docker stop $(NAME)
shell:
docker run --rm -it \
--name $(CONTAINER)-shell \
-v "$(shell pwd):$(WORKDIR)" \
-w $(WORKDIR) \
$(IMAGE) /bin/sh
help:
@printf "Usage:\n make build Build local docker image ($(IMAGE))\n make run Run image mounting project into $(WORKDIR) (port $(PORT))\n make stop Stop running container\n make shell Start a shell in a container with project mounted\n\nOverride variables: IMAGE, CONTAINER, WORKDIR, PORT, DOCKER_BUILD_ARGS, DOCKER_RUN_ARGS\n"

73
ReadMe.md Normal file
View File

@ -0,0 +1,73 @@
# ImapNotification
Lightweight utility to monitor an IMAP mailbox and emit notifications when new messages arrive. Intended to be used as a local helper, service, or integration point for scripts/webhooks.
## Features
- Monitor one or more IMAP accounts/folders
- Deliver notifications via command hook, webhook, or stdout
- Support for polling and IMAP IDLE (when available)
- Basic filtering by folder, sender, or subject patterns
## Quick start
1. Clone the repository:
```
git clone <repo-url>
cd ImapNotification
```
2. Provide configuration (environment variables or YAML/JSON). Example env vars:
```
IMAP_HOST=imap.example.com
IMAP_PORT=993
IMAP_TLS=true
IMAP_USER=user@example.com
IMAP_PASS=yourpassword
MAILBOX=INBOX
POLL_INTERVAL=30
NOTIFY_COMMAND=/path/to/notify-script.sh
USE_IDLE=true
```
Or a minimal YAML example (config.yml):
```yaml
accounts:
- host: imap.example.com
port: 993
tls: true
user: user@example.com
pass: secret
mailboxes:
- INBOX
settings:
poll_interval: 30
use_idle: true
notify_command: /usr/local/bin/notify.sh
```
3. Run the program:
```
./imap-notification --config config.yml
```
(or use the provided run/build instructions for this project)
## Configuration
- host, port, tls, user, pass: IMAP connection details
- mailboxes: list of folders to monitor
- poll_interval: seconds between checks when not using IDLE
- use_idle: enable IMAP IDLE if server supports it
- notify_command / webhook: how to deliver notifications
- filters: optional sender/subject/folder filters
## Logging & debugging
- Enable verbose logging to troubleshoot connection or authentication issues.
- Check firewall and IMAP server settings if connections fail.
## Contributing
- Fork, create a feature branch, add tests, and submit a pull request.
- Keep changes small and documented. Include rationale for new configuration options.
## License
Specify your license in LICENSE file (e.g., MIT).
For more detailed usage and examples, see the project docs or examples folder.

1
project.env Normal file
View File

@ -0,0 +1 @@
NAME=imap_notification

12
src/config.json.example Executable file
View File

@ -0,0 +1,12 @@
{
"lang": "en",
"accounts": [
{
"name": "Example",
"host": "imap.exmaple.com",
"user": "example@example.com",
"pass": "examplepassword",
"topic": "your-ntfy-topic"
}
]
}

33
src/i18n.js Normal file
View File

@ -0,0 +1,33 @@
const fs = require('fs');
const path = require('path');
let translations = {};
let currentLang = 'en';
function load(lang) {
try {
const p = path.join(__dirname, 'lang', `${lang}.json`);
const raw = fs.readFileSync(p, 'utf8');
return JSON.parse(raw);
} catch (e) {
if (lang !== 'en') return load('en');
return {};
}
}
function init(lang) {
currentLang = lang || 'en';
translations = load(currentLang);
}
function t(key, vars) {
let str = translations[key] || key;
if (vars) {
Object.keys(vars).forEach(k => {
str = str.replace(new RegExp(`\{${k}\}`, 'g'), vars[k]);
});
}
return str;
}
module.exports = { init, t };

13
src/lang/en.json Normal file
View File

@ -0,0 +1,13 @@
{
"app_started": "ImapNotification application started.",
"message_body": "{fromIcon}: {from}\n{toIcon}: {to}\n{subIcon}: {subject}",
"new_email_title": "New email received",
"ntfy_call_error": "Error calling ntfy:",
"watch_started": "Watching (IDLE) started...",
"watch_error": "An error occurred:",
"conn_closed": "Connection closed, restarting...",
"no_subject": "No subject",
"unknown_recipient": "Unknown recipient",
"no_accounts": "No accounts defined in config.",
"missing_account_params": "Missing required account parameters: host, user, pass, topic, name"
}

16
src/lang/hu.json Normal file
View File

@ -0,0 +1,16 @@
{
"error_reading_config": "Error reading config file: {path}, message: {message}",
"new_email_title": "Új Email érkezett",
"notif_sent": "Noti elküldve a saját ntfy-nek.",
"ntfy_call_error": "Hiba a belső ntfy hívásnál:",
"watch_started": "Figyelés indítva (IDLE)...",
"new_email_log": "Új email tőle: {from}, neki: {to}, tárgy: {subject}",
"watch_error": "Hiba történt:",
"conn_closed": "Kapcsolat bezárult, újraindítás...",
"app_started": "ImapNotification alkalmazás elindult.",
"message_body": "{fromIcon}: {from}\n{toIcon}: {to}\n{subIcon}: {subject}",
"no_subject": "Nincs tárgy",
"unknown_recipient": "Ismeretlen címzett",
"no_accounts": "Nincs fiók definiálva a konfigurációban.",
"missing_account_params": "Hiányzó kötelező fiók paraméterek: host, user, pass, topic, name"
}

154
src/main.js Executable file
View File

@ -0,0 +1,154 @@
const { ImapFlow } = require('imapflow');
const axios = require('axios');
const fs = require('fs');
const i18n = require('./i18n');
const NTFY_INTERNAL_URL = process.env.NTFY_URL || 'https://ntfy.sh';
const CONFIG_PATH = process.env.CONFIG_PATH || './config.json';
const LOG_LEVEL = process.env.LOG_LEVEL || 'info';
let config;
const fromIcon = "\uD83D\uDC64"; // 👤
const toIcon = "\uD83D\uDCE5"; // 📥
const subjIcon = "\u2709\uFE0F"; // ✉️
const log = {
info: (...args) => console.log('[INFO] ', ...args),
error: (...args) => console.error('[ERROR]', ...args),
debug: (...args) => console.debug('[DEBUG]', ...args)
}
switch (LOG_LEVEL) {
case 'info':
log.debug = () => { };
break;
case 'error':
log.debug = () => { };
log.info = () => { };
break;
case 'debug':
default:
break;
}
try {
const rawData = fs.readFileSync(CONFIG_PATH);
config = JSON.parse(rawData);
} catch (err) {
log.error(`Error reading config file: ${CONFIG_PATH}, message: `, err.message);
process.exit(1);
}
i18n.init(config.lang || 'en');
async function sendNtfy(acc, subject, from, to) {
try {
const messageBody = i18n.t('message_body', {
fromIcon: fromIcon,
toIcon: toIcon,
subIcon: subjIcon,
from: from,
to: to,
subject: subject
});
log.debug(`Sending ntfy notification for account [${acc.name}] with topic [${acc.topic}]`, messageBody);
await axios.post(`${NTFY_INTERNAL_URL}`, {
topic: acc.topic,
title: i18n.t('new_email_title'),
message: messageBody,
priority: 5,
tags: ['incoming_envelope'],
click: 'message://'
})
} catch (err) {
log.error(i18n.t('ntfy_call_error'), err.message);
}
}
async function validateAccountParams(params) {
log.debug('Validating account params: ', params);
if (!params.host || !params.user || !params.pass || !params.topic || !params.name) {
throw new Error(i18n.t('missing_account_params'));
}
}
async function watchAccount(acc) {
const client = new ImapFlow({
host: acc.host,
port: 993,
secure: true,
auth: {
user: acc.user,
pass: acc.pass
},
logger: (LOG_LEVEL === 'debug' ? log.debug : false)
});
const run = async () => {
try {
await client.connect();
let lock = await client.getMailboxLock('INBOX');
log.info(`[${acc.name}] ${i18n.t('watch_started')}`);
client.on('exists', async (data) => {
// Fetch only envelope data (Subject, From)
let message = await client.fetchOne(data.count, { envelope: true });
const subject = message.envelope.subject || i18n.t('no_subject');
const fromInfo = message.envelope.from[0];
const fromDisplay = fromInfo.name ? fromInfo.name : fromInfo.address;
// Extract recipient(s)
// The 'envelope.to' is an array because there can be multiple recipients
const toInfo = message.envelope.to && message.envelope.to[0];
const toAddress = toInfo ? toInfo.address : i18n.t('unknown_recipient');
await sendNtfy(acc, subject, fromDisplay, toAddress);
});
} catch (err) {
log.error(`[${acc.name}] ${i18n.t('watch_error')}`, err.message);
process.exit(1);
}
};
run();
client.on('close', () => {
log.info(`[${acc.name}] ${i18n.t('conn_closed')}`);
process.exit(1);
});
}
async function run(config) {
log.info(i18n.t('app_started'));
if (!Array.isArray(config.accounts) || config.accounts.length === 0) {
log.error(i18n.t('no_accounts'));
process.exit(1);
}
for (const acc of config.accounts) {
try {
await validateAccountParams(acc);
} catch (err) {
log.error(`[${acc.name || 'Unnamed'}] ${err.message}`);
process.exit(1);
}
}
for (const acc of config.accounts) {
watchAccount(acc);
}
}
run(config);

27
src/package.json Executable file
View File

@ -0,0 +1,27 @@
{
"name": "imapnotification",
"version": "0.1.0",
"description": "IMAP -> ntfy notification bridge",
"main": "index.js",
"type": "commonjs",
"scripts": {
"start": "node index.js",
"dev": "nodemon src/main.ts",
"build": "tsc -p .",
"lint": "eslint . --ext .js,.ts"
},
"engines": {
"node": ">=18"
},
"dependencies": {
"imapflow": "^1.0.0",
"axios": "^1.5.0"
},
"devDependencies": {
"typescript": "^5.0.0",
"ts-node": "^10.0.0",
"nodemon": "^2.0.0",
"eslint": "^8.0.0"
},
"license": "MIT"
}