Skip to content
Merged
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
23 changes: 11 additions & 12 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
# Run tests and lint inside node containers
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs

name: Node.js CI

Expand All @@ -16,15 +16,14 @@ jobs:

strategy:
matrix:
node-version: [14.x, 15.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
node-version: [18-alpine, 20-alpine, 22-alpine]

container:
image: node:${{ matrix.node-version }}

steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm test
- run: npm run lint
- uses: actions/checkout@v4
- run: apk add --no-cache make
- run: make install
- run: make test
Comment on lines +19 to +28
- run: make lint
45 changes: 45 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
IMAGE := node:20-alpine
WORKDIR := /app

## Detect container engine (podman or docker)
CONTAINER_ENGINE := $(shell command -v podman 2>/dev/null || command -v docker 2>/dev/null)

# Use :Z bind-mount option only with Podman (SELinux relabeling)
ifneq (,$(findstring podman,$(CONTAINER_ENGINE)))
MOUNT_OPT := :Z
endif

VOLUME := -v $(PWD):$(WORKDIR)$(MOUNT_OPT)

.PHONY: shell test lint install clean

install:
ifdef CONTAINER_ENGINE
$(CONTAINER_ENGINE) run --rm $(VOLUME) -w $(WORKDIR) $(IMAGE) npm install
else
npm install
endif

test:
ifdef CONTAINER_ENGINE
$(CONTAINER_ENGINE) run --rm $(VOLUME) -w $(WORKDIR) $(IMAGE) npx mocha
else
npx mocha
endif

lint:
ifdef CONTAINER_ENGINE
$(CONTAINER_ENGINE) run --rm $(VOLUME) -w $(WORKDIR) $(IMAGE) npx eslint .
else
npx eslint .
endif

shell:
ifdef CONTAINER_ENGINE
$(CONTAINER_ENGINE) run --rm -it $(VOLUME) -w $(WORKDIR) $(IMAGE) sh
else
@echo "no container engine available"
endif

clean:
rm -rf node_modules
73 changes: 46 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@
3. Click "Run" icon at the top of the editor, you will be prompted to grant access to your gmail.
4. If there are any messages in your inbox that have header "List-Id" it will create and assign labels.

**NOTE**:
**NOTE**:
- Missing labels are created automatically.
- Feel free to delete labels in gmail at any time, they won't be recreated until matching email is processed by Ukko.
- Labels separated by "/" are nested.
- Threads are assigned all labels in the nested chain, for label "lists/my-list/company", following are assigned:
- "lists"
- "lists/my-list"
- "lists/my-list/company"
- "lists/my-list/company"
- Labelled threads are shown at all nesting levels.

**NOTE**: To automatically "archive" the thread after labels are applied, uncomment following line in [modules/ukko.js](https://github.com/T0MASD/ukko/blob/main/modules/ukko.js) :
Expand Down Expand Up @@ -119,46 +119,65 @@ Eslint is used for linting all js code, if you have installed [eslint](https://e
```

## Run Ukko
To Ukko locally with mock data call `node .` or `npm start`
To run Ukko locally with mock data call `node app.js` or `npm start`
```shell
[tomas@dev ukko]$ npm start

> ukko@0.0.1 start /ukko
> node .
> node app.js

from:email@example.com labels:lists/planet-list
from:announce-list@example.com labels:lists/announce-list
from:Announce list <announce-list@example.com> labels:lists/announce-list/example
from:email@subdomain.example.com labels:lists/planet-list/example
```

## Extending label rules
Modify `getLabels` function of [modules/ukko.js](https://github.com/T0MASD/ukko/blob/main/modules/ukko.js) to add your own logic for setting up labels. Few example rules are included with ukko, feel free to change to suit your needs.
Ukko uses a config-driven rules engine. To add or modify label rules, edit the `RULES` array in [modules/ukko.js](https://github.com/T0MASD/ukko/blob/main/modules/ukko.js).

### Rule fields

Create "catch all" filters for given domain:
| Field | Description |
|-------|-------------|
| `header` | Email header name to check (`From`, `Sender`, `To`, `List-Id`, `X-GitLab-Project`, etc.) |
| `contains` | Substring match against header value |
| `endswith` | Suffix match against header value |
| `label` | Static label to assign (or base label for handlers) |
| `handler` | Name of handler function for dynamic sublabeling |
| `fallback` | Only apply if no labels matched yet (default: `false`) |

### Adding a simple rule
Match emails from a domain and assign a static label:
```javascript
// process @github.com
if (messageFrom.includes('@github.com')) {
labels.push('github')
}
{ header: 'From', contains: '@github.com', label: 'github' }
```
Here's an sample to extract GitLab project name from header named `X-GitLab-Project` and assign label `gitlab/projectname`:

### Adding a rule with a handler
Handlers receive `(message, baseLabel)` and return an array of labels. They can extract dynamic sublabels from other headers:
```javascript
// process gitlab notifications
if (message.getHeader('X-GitLab-Project')) {
labels.push(`gitlab/${message.getHeader('X-GitLab-Project')}`)
// Rule config
{ header: 'From', contains: '@github.com', label: 'github', handler: 'github' }

// Handler in HANDLERS object
github: function (message, baseLabel) {
let label = baseLabel
const toValue = message.getHeader('To')
if (toValue) {
const ghProj = getReMatch('to', toValue)
if (ghProj) { label += `/${ghProj}` }
}
return [label]
}
```
Here's a sample to assign label from the value of `List-Id` header, in this example extra label is added where sender domain doesn't match `mydomain`:

### Adding a fallback rule
Fallback rules only fire when no other rules have matched:
```javascript
// process mailing lists
if (message.getHeader('List-Id')) {
// extract my-list from 'My List <my-list.example.com>'
const listIDshort = getReMatch('listid', message.getHeader('List-Id'))
let listLabel = 'lists/' + listIDshort
if (messageFromDomain !== 'mydomain') {
listLabel += `/${messageFromDomain}`
}
labels.push(listLabel)
}
{ header: 'List-Id', label: 'lists', handler: 'mailing_list', fallback: true }
```

### Adding a handler-only rule (no pattern)
Rules with a handler but no `contains`/`endswith` always run when the header exists:
```javascript
{ header: 'X-GitLab-Project', label: 'gitlab', handler: 'gitlab_project' }
```
## Contributions

Expand Down
7 changes: 6 additions & 1 deletion app.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,9 @@ const logger = new Logger()

export { gmailApp as GmailApp, logger as Logger }

runUkko()
// run when executed directly: node app.js
// skip when imported by test runner (mocha, etc.)
const entryArg = process.argv[1] || ''
if (entryArg.endsWith('app.js')) {
runUkko()
}
Loading
Loading