Skip to content

Commit

Permalink
Show new message notifications, add support for Waybar
Browse files Browse the repository at this point in the history
  • Loading branch information
crabvk committed Sep 24, 2023
1 parent 079535e commit 22e7535
Show file tree
Hide file tree
Showing 16 changed files with 429 additions and 165 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1 @@
credentials.json
/dist
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The MIT License

Copyright (c) 2022 Vyacheslav Konovalov https://github.com/crabvk
Copyright (c) 2023 Vyacheslav Konovalov https://github.com/crabvk

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
106 changes: 68 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,67 +1,97 @@
# Polybar Gmail

A [Polybar](https://github.com/jaagr/polybar) module to show unread messages from Gmail.
# Bar Gmail

![preview](https://github.com/crabvk/polybar-gmail/raw/master/preview.png)

Get notifications and unread messages count from Gmail (Waybar/Polybar module).

## Dependencies

```sh
pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib
# or use poetry
```
* Font Awesome: default badge 
* Libnotify: new email notifications, can be disabled with `--no-notify` flag.
* Libcanberra: notification sound (optional).

To display notifications you must have a [notification daemon](https://wiki.archlinux.org/title/Desktop_notifications#Notification_servers) running on your system.

**Font Awesome** - default email icon
## Install

**canberra-gtk-play** - new email sound notification
### ArchLinux and derivatives

You can change the icon or turn off sound, for more info see [script arguments](#script-arguments)
[AUR package](https://aur.archlinux.org/packages/bar-gmail/)

## Installation
### Other distros

```sh
cd ~/.config/polybar
curl -LO https://github.com/crabvk/polybar-gmail/archive/master.tar.gz
tar zxf master.tar.gz && rm master.tar.gz
mv polybar-gmail-master gmail
git clone https://github.com/crabvk/bar-gmail.git
cd bar-gmail
git describe --abbrev=0 --tags # Get latest tag.
git checkoug LATEST_TAG
pip install -e .
```

and obtain/refresh credentials
And now you can execute *~/.local/bin/bar-gmail*

```sh
~/.config/polybar/gmail/auth.py
```
## Usage

### Module
First, you need to authenticate the client:

```ini
[module/gmail]
type = custom/script
exec = ~/.config/polybar/gmail/launch.py
tail = true
click-left = xdg-open https://mail.google.com
```sh
bar-gmail auth
```

## Script arguments
Then just run `bar-gmail` or `bar-gmail --format polybar` periodically to get unread messages count and new message notifications.
Credentials and session are stored in *~/.cache/bar-gmail*.

`-l` or `--label` - set user's mailbox [label](https://developers.google.com/gmail/api/v1/reference/users/labels/list), default: INBOX
## Waybar config example

`-p` or `--prefix` - set email icon, default: 
*~/.config/waybar/config*

`-c` or `--color` - set new email icon color, default: #e06c75
```json
"modules-right": {
"custom/gmail"
}

`-ns` or `--nosound` - turn off new email sound
"custom/gmail": {
"exec": "bar-gmail",
"return-type": "json",
"interval": 10,
"tooltip": false,
"on-click": "xdg-open https://mail.google.com/mail/u/0/#inbox"
}
```

`-cr` or `--credentials` - path to your `credentials.json`, defaults to `credentials.json`
*~/.config/waybar/style.css*

```css
#custom-gmail.unread {
color: white;
}
#custom-gmail.inaccurate {
color: darkorange;
}
#custom-gmail.error {
color: darkred;
}
```

### Example
## Polybar config example

```sh
./launch.py --label 'CATEGORY_PERSONAL' --prefix '' --color '#be5046' --nosound
```ini
modules-right = gmail
...
[module/gmail]
type = custom/script
exec = bar-gmail -f polybar
interval = 10
click-left = xdg-open https://mail.google.com/mail/u/0/#inbox
```

## Get list of all your mailbox labels
## Script arguments

```python
./list_labels.py
See `bar-gmail --help` for the full list of available subcommands and command arguments.
Possible values for `-s`, `--sound` can be obtained with:

```shell
ls /usr/share/sounds/freedesktop/stereo/ | cut -d. -f1
```

for example `bar-gmail --sound message-new-instant`.
27 changes: 0 additions & 27 deletions auth.py

This file was deleted.

77 changes: 77 additions & 0 deletions bar_gmail/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import sys
import argparse
from pathlib import Path
from bar_gmail.gmail import Gmail
from bar_gmail.app import Application, UrgencyLevel
from bar_gmail.printer import WaybarPrinter, PolybarPrinter


def cli():
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='subcommand')
subparsers.add_parser('auth', help='Authentication.')
subparsers.add_parser('labels', help='List mailbox labels.')
parser.add_argument('-f', '--format', choices=['waybar', 'polybar'], default='waybar',
help='Print output in specified format [default: waybar].')
parser.add_argument('-b', '--badge', default='',
help='Badge to display in the bar [default: ].')
parser.add_argument('-c', '--color',
help='Text foreground color (only for Polybar).')
parser.add_argument('-l', '--label', default='INBOX',
help="User's mailbox label for unread messages count [default: INBOX].")
parser.add_argument('-s', '--sound',
help='Notification sound (event sound ID from canberra-gtk-play).')
parser.add_argument('-u', '--urgency', choices=['low', 'normal', 'critical'],
default='normal', help='Notification urgency level [default: normal].')
parser.add_argument('-t', '--expire-time', type=int,
help='The duration, in milliseconds, for the notification to appear on screen.')
parser.add_argument('-dn', '--no-notify', action='store_true',
help='Disable new email notifications.')
args = parser.parse_args()

if args.color is not None and args.format != 'polybar':
parser.error('`--color COLOR` can be used only with `--format polybar`.')

BASE_DIR = Path(__file__).resolve().parent
CLIENT_SECRETS_PATH = Path(BASE_DIR, 'client_secrets.json')
CACHE_DIR = Path(Path.home(), '.cache/bar-gmail')
CREDENTIALS_PATH = Path(CACHE_DIR, 'credentials.json')
SESSION_PATH = Path(CACHE_DIR, 'session.json')

if not CACHE_DIR.is_dir():
CACHE_DIR.mkdir(exist_ok=True)

if not CREDENTIALS_PATH.is_file():
print('Credentials not found. Run `bar-gmail auth` for authentication.', file=sys.stderr)
exit(1)

gmail = Gmail(CLIENT_SECRETS_PATH, CREDENTIALS_PATH)

if args.subcommand == 'auth':
if gmail.authenticate():
print('Authenticated successfully.')
exit()

if args.subcommand == 'labels':
for label in gmail.get_labels():
print(label)
exit()

if args.format == 'waybar':
printer = WaybarPrinter(badge=args.badge)
elif args.format == 'polybar':
printer = PolybarPrinter(badge=args.badge, color=args.color)

app = Application(SESSION_PATH, gmail, printer,
badge=args.badge,
color=args.color,
label=args.label,
sound_id=args.sound,
urgency_level=UrgencyLevel(args.urgency),
expire_time=args.expire_time,
is_notify=not args.no_notify)
app.run()


if __name__ == '__main__':
cli()
104 changes: 104 additions & 0 deletions bar_gmail/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import os
import json
import time
from enum import Enum
from pathlib import Path
from subprocess import Popen
from bar_gmail.gmail import Gmail
from bar_gmail.printer import WaybarPrinter, PolybarPrinter
from google.auth.exceptions import TransportError
from googleapiclient.errors import HttpError

BASE_DIR = Path(__file__).resolve().parent
GMAIL_ICON_PATH = Path(BASE_DIR, 'gmail_icon.svg')


class UrgencyLevel(Enum):
LOW = 'low'
NORMAL = 'normal'
CRITICAL = 'critical'


class Application:
def __init__(self, session_path: Path, gmail: Gmail, printer: WaybarPrinter | PolybarPrinter,
badge: str, color: str | None, label: str, sound_id: str,
urgency_level: UrgencyLevel, expire_time: int, is_notify: bool):
self.session_path = session_path
self.gmail = gmail
self.printer = printer
self.badge = badge
self.label = label
self.sound_id = sound_id
self.urgency_level = urgency_level
self.expire_time = expire_time
self.is_notify = is_notify
self.color = color
args = []
# Set application name.
args.extend(('-a', 'Bar Gmail'))
# Set category.
args.extend(('-c', 'email.arrived'))
# Set icon.
args.extend(('-i', GMAIL_ICON_PATH))
# Set urgency level.
args.extend(('-u', self.urgency_level.value))
# Set notification expiration time.
if self.expire_time is not None:
args.extend(('-t', self.expire_time))
self.notification_args = args

@staticmethod
def _is_innacurate(since: float) -> bool:
# Data older than 5 minutes is considered innacurate.
return time.time() - since > 300

def _play_sound(self):
try:
Popen(['canberra-gtk-play', '-i', self.sound_id], stderr=open(os.devnull, 'wb'))
except FileNotFoundError:
pass

def _send_notification(self, message):
try:
Popen(['notify-send', *self.notification_args, message['From'], message['Subject']],
stderr=open(os.devnull, 'wb'))
except FileNotFoundError:
pass

def run(self):
session = {'history_id': None, 'unread': None}
inaccurate = False
if self.session_path.is_file():
with open(self.session_path, 'r') as f:
session = json.loads(f.read())
inaccurate = self._is_innacurate(session['time'])
self.printer.print(session['unread'], inaccurate=inaccurate)

try:
unread = self.gmail.get_unread_messages_count(self.label)
if unread != session['unread'] or inaccurate == True:
self.printer.print(unread)
history_id = session['history_id'] or self.gmail.get_latest_history_id()
session = {
'history_id': history_id,
'unread': unread,
'time': time.time()
}
with open(self.session_path, 'w') as f:
json.dump(session, f)

if session['history_id']:
history = self.gmail.get_history_since(session['history_id'])
if any(history['messages']) and self.sound_id:
self._play_sound()
for message in history['messages']:
print(message)
self._send_notification(message)
session['history_id'] = history['history_id']
with open(self.session_path, 'w') as f:
json.dump(session, f)
except HttpError as error:
if error.resp.status == 404:
self.printer.error(f'Label not found: {self.label}')
except TransportError:
pass
1 change: 1 addition & 0 deletions bar_gmail/client_secrets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"installed":{"client_id":"435972834621-7lmn42355puhs92f7jifob4vd46mn73l.apps.googleusercontent.com","project_id":"bar-gmail","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"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","redirect_uris":["http://localhost"]}}
Loading

0 comments on commit 22e7535

Please sign in to comment.