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:
commit
b92ec64746
1
.copilotignore
Normal file
1
.copilotignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
src/config.json
|
||||||
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
config.json
|
||||||
35
.gitignore
vendored
Normal file
35
.gitignore
vendored
Normal 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
18
Dockerfile
Normal 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
47
Makefile
Normal 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
73
ReadMe.md
Normal 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
1
project.env
Normal file
@ -0,0 +1 @@
|
|||||||
|
NAME=imap_notification
|
||||||
12
src/config.json.example
Executable file
12
src/config.json.example
Executable 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
33
src/i18n.js
Normal 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
13
src/lang/en.json
Normal 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
16
src/lang/hu.json
Normal 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
154
src/main.js
Executable 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
27
src/package.json
Executable 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"
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user