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