├── .gitignore
├── Dockerfile
├── Makefile
├── README.md
├── autoload
└── health
│ └── galore.vim
├── doc
├── galore.txt
└── tags
├── examples
├── gpg-tui.lua
├── keybindings.lua
├── khard.lua
└── snippets.lua
├── galore.desktop
├── install_local.sh
├── lua
└── galore
│ ├── address-util.lua
│ ├── autocrypt
│ └── init.lua
│ ├── browser.lua
│ ├── builder.lua
│ ├── callback.lua
│ ├── compose.lua
│ ├── config.lua
│ ├── crypt-utils.lua
│ ├── debug.lua
│ ├── default.lua
│ ├── gmime-util.lua
│ ├── health.lua
│ ├── hooks.lua
│ ├── hooks
│ ├── post.lua
│ └── send.lua
│ ├── init.lua
│ ├── jobs.lua
│ ├── lib
│ ├── buffer.lua
│ ├── ordered.lua
│ └── timers.lua
│ ├── log.lua
│ ├── message_browser.lua
│ ├── message_view.lua
│ ├── notmuch-util.lua
│ ├── notmuch.lua
│ ├── opts.lua
│ ├── render.lua
│ ├── runtime.lua
│ ├── saved.lua
│ ├── telescope.lua
│ ├── templates.lua
│ ├── test_utils.lua
│ ├── thread_browser.lua
│ ├── thread_message_browser.lua
│ ├── thread_view.lua
│ ├── ui.lua
│ ├── url.lua
│ ├── util.lua
│ └── views.lua
├── meson.build
├── plugin
└── galore.lua
├── res
├── galore.svg
├── message_view.png
├── overview.png
├── saved.png
├── telescope.png
└── thread_message.png
├── src
├── galore-autocrypt.c
├── galore-autocrypt.h
├── galore-filter-reply.c
├── galore-filter-reply.h
├── galore-gpgme-utils.c
├── galore-gpgme-utils.h
├── galore.c
├── galore.h
└── meson.build
├── stylua.toml
├── syntax
├── galore-browser.vim
├── galore-saved.vim
└── galore-threads.vim
├── test
├── ci_init.sh
├── init.sh
├── minimal.vim
└── test
│ └── init_spec.lua
└── todo.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .envrc
2 | .cache/
3 | build/
4 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ubuntu:latest
2 |
3 | # Make code directory
4 | RUN mkdir -p code/galore
5 |
6 | # update, software-properties-common, git
7 | RUN apt-get update && \
8 | apt install -y software-properties-common && \
9 | apt install -y git && \
10 | apt install -y curl && \
11 | apt install -y build-essential && \
12 | apt install -y luarocks &&\
13 | apt install -y notmuch &&\
14 | apt install -y libnotmuch-dev &&\
15 | apt install -y gobject-introspection &&\
16 | apt install -y libgirepository1.0-dev &&\
17 | apt install -y libgmime-3.0-dev &&\
18 | apt install -y meson
19 |
20 | RUN add-apt-repository --yes ppa:neovim-ppa/unstable && \
21 | apt-get install -y neovim
22 |
23 | # RUN luarocks install argparse && luarocks install luacheck
24 | RUN luarocks install luacheck
25 | RUN luarocks --lua-version 5.1 install lgi
26 | RUN mkdir -p /tmp/
27 | WORKDIR /tmp
28 |
29 | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
30 | ENV PATH="/root/.cargo/bin:${PATH}"
31 |
32 | RUN cargo install stylua
33 | RUN cargo install lemmy-help --features=cli
34 |
35 | # 'nvim-lua/popup.nvim',
36 |
37 | # 'hrsh7th/nvim-cmp', -- optional
38 | # 'dagle/cmp-notmuch', -- optional
39 | # 'dagle/cmp-mates', -- optional
40 |
41 | # Clone dependencies
42 | RUN git clone https://github.com/nvim-lua/plenary.nvim.git /code/plenary.nvim
43 | RUN git clone https://github.com/nvim-treesitter/nvim-treesitter.git /code/nvim-treesitter
44 | RUN git clone https://github.com/nvim-telescope/telescope.nvim.git /code/telescope
45 | RUN git clone https://github.com/nvim-telescope/telescope-file-browser.nvim.git /code/filebrowser
46 | RUN git clone https://github.com/dagle/notmuch-lua.git /code/notmuch-lua
47 |
48 | # Run tests when run container
49 | # CMD bash
50 | CMD cd /code/galore && \
51 | make test
52 | # make lint && \
53 | # make stylua && \
54 | # make emmy && \
55 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | THIS_DIR := $(dir $(abspath $(firstword $(MAKEFILE_LIST))))
2 |
3 | install:
4 | # do xdg etc
5 | install -d ${HOME}/.local/share/icons/scalable/apps/ ${HOME}/.local/share/applications/
6 | install galore.desktop ${HOME}/.local/share/applications/
7 | install res/galore.svg ${HOME}/.local/share/icons/scalable/apps/
8 | update-desktop-database ~/.local/share/applications
9 | # xdg-mime default galore.desktop x-scheme-handler/mailto
10 |
11 | lint:
12 | luacheck lua
13 | # add linting to src/ for C
14 |
15 | stylua:
16 | stylua lua/
17 |
18 | format:
19 | clang-format --style=file --dry-run -Werror src/.c src/.h
20 |
21 | # TODO finnish this
22 | # gdi:
23 | # gir-to-vimdoc
24 |
25 | test/testdir:
26 | test/init.sh
27 |
28 | # this will kinda ruin your install if you run ready-pod
29 | # Fix this later
30 | test: test/testdir
31 | ./install_local.sh
32 | LUA_PATH="/usr/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?.lua" nvim --headless --clean \
33 | -u test/minimal.vim \
34 | -c "PlenaryBustedDirectory test/test {minimal_init = 'test/minimal.vim'}"
35 |
36 | pod-build:
37 | podman build . -t galore
38 | # podman build --no-cache . -t galore
39 |
40 | ready-pod:
41 | podman run -v $(shell pwd):/code/galore -t galore
42 |
43 | clean:
44 | $(RM) -r build
45 | $(RM) -r test/testdir
46 |
47 | .PHONY: lint format stylua clean test debug
48 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
19 |
20 | ## Intro
21 | Notmuch is a great way to index your email and neovim is great for viewing and editing
22 | text. Combine the two and you have a great email client.
23 | The idea is to be more powerful than mutt but way easier to configure.
24 |
25 | Other than the basic notmuch features Galore has support for:
26 | - cmp for address completion
27 | - telescope for fuzzy searching emails
28 | - encrypting and decrypting emails
29 | - signing and verifying emails
30 |
31 | The idea is to provide a good email experiance by default where you configure
32 | every everything to your needs
33 |
34 | ## Installation
35 | WIP, mail galore is under heavy development, expect crashes and thing changing.
36 | Atm it's pre 0.1 software. If you don't intend to read code / write patches, you should wait.
37 | Galore requires the git branch of neovim or 0.7 to work.
38 |
39 | Galore uses luajit and the C-bindings to do its magic, depending on notmuch and gmime.
40 | To view html emails, you need a htmlviewer, by default it uses w3m.
41 |
42 | To be able to run qeuries async it uses another program called [nm-livesearch](https://github.com/dagle/nm-livesearch),
43 | it's used to both populate browser windows and telescope. You *need* to install it before using galore
44 |
45 | Some html emails are just to impossible to render in galore (or any text email client), for these
46 | you need full blown webbrowser. For this you can use a tool like
47 | [browser-pipe](https://github.com/dagle/browser-pipe) and then pipe the html into it.
48 | (look in config for an example)
49 |
50 | You need to install neovim and notmuch.
51 |
52 | Then using your favorite plugin-manager install galore.
53 |
54 | With packer:
55 | ``` lua
56 | use {'dagle/galore', run = 'bash install_local.sh',
57 | rocks = {'lgi'}, -- or install lgi with your package manager, doesn't seem to work with packer atm
58 | requires = {
59 | 'nvim-telescope/telescope.nvim',
60 | 'nvim-telescope/telescope-file-browser.nvim',
61 | 'nvim-lua/popup.nvim',
62 | 'nvim-lua/plenary.nvim',
63 | 'dagle/notmuch-lua',
64 | 'hrsh7th/nvim-cmp', -- optional
65 | 'dagle/cmp-notmuch', -- optional
66 | 'dagle/cmp-mates', -- optional
67 | }
68 | }
69 | ```
70 | You need to install **telescope** and **cmp** if you want support for that
71 |
72 | It also has optional support for the address book mates, so install that for
73 | mates support (will be moved to a seperate plugin later)
74 |
75 | After installing do
76 | :checkhealth galore
77 | To make sure everything is working
78 |
79 | ## Usage
80 | After installing galore, you need to add the following to init:
81 | ``` lua
82 | local galore = require('galore')
83 | galore.setup()
84 | ```
85 | and when you want to launch galore do:
86 | ``` lua
87 | galore.open
88 | ```
89 | or
90 | ```
91 | :Galore
92 | ```
93 |
94 | By default, galore tries to read values from the notmuch config file.
95 | You can also override options by passing values to setup, look in config.lua
96 | for default values (will be documented in the future).
97 |
98 | ### Telescope
99 | Galore exports the following telescope functions (require 'galore.telescope' to use them)
100 | - notmuch_search
101 | - load_draft
102 | - attach_file (only works in compose)
103 | - save_files (only works in message_view)
104 |
105 | ### Cmp
106 | Galore has 2 ways to find emails addresses,
107 | first uses mates vcard system and seconds uses notmuch to fetch addresses.
108 |
109 | Add
110 | ``` lua
111 | { name = 'vcard_addr'},
112 | ```
113 | and/or
114 | ``` lua
115 | {name = 'notmuch_addr'}
116 | ```
117 | to you sources and update your formatting
118 |
119 | ## Pictures
120 | Saved searches
121 |
122 |
123 |
124 | Thread message browser
125 |
126 |
127 |
128 | Message view
129 |
130 |
131 |
132 | Telescope
133 |
134 |
135 |
136 | And with a couple of windows together
137 |
138 |
139 |
140 | ## Customize
141 |
142 | Global functions
143 | ----------------
144 | Most of galore functionallity can be ran globaly, from anywhere but you want to make sure
145 | that libs are loaded and that we setup variables (they are lazy loaded).
146 | Here is an example of a function that connects (if not connected) and do a telescope search:
147 |
148 | ``` lua
149 | vim.keymap.set('n', 'ms', function()
150 | require("galore").withconnect(function ()
151 | require("galore.telescope").notmuch_search() end)
152 | end, {desc='Search email'})
153 | ```
154 | Inside of galore you can call require("galore.telescope").notmuch_search() directly.
155 |
156 |
157 | Some jobs doesn't require notmuch to be started:
158 | ``` lua
159 | require("galore.jobs").new()
160 | ```
161 |
162 | config values
163 | -------------
164 | A lot of values are loaded from the notmuch config but
165 | can be overridden. I reccomend setting values in the notmuch config
166 | and let galore fetch them from there. That way you will not need to
167 | write values to multiple places.
168 |
169 | For settings to change, look in lua/galore/config.lua
170 | for values to change with setup.
171 | They will be documented here in the future.
172 |
173 | For cmp support you need to install cmp completers (see install above)
174 |
175 | For cmp support you need to add the following:
176 | { name = 'vcard_addr'},
177 | { name = 'notmuch_addr'},
178 | to your cmp sources
179 |
180 | views
181 | -----
182 | If you want to customize colors, just change the Galore* highlights.
183 |
184 | If you wanna customize how galore is rendered, you change the print
185 | functions and also the syntax files. You need to create your own
186 | syntax/galore-browser.vim that matches your syntax.
187 |
188 | If you only wanna customize the colors, you can just
189 |
190 | Buffers
191 | -------
192 |
193 | Galore comes with a couple of different buffers to navigate your email
194 |
195 | Saved is a buffer for saved searches, selecting a saved search will run that search
196 | in a selected browsers. Saved accepts a list of generators that produce output.
197 | gen_tags: Take all tags the db uses and display them one by one.
198 | gen_internal: All searches we have saved internally and
199 | don't want to export into the ecosystem (saved in a file)
200 | gen_excluded: tags excluded from our searches. For example if we exclude archive,
201 | then archive will gets it's own entry
202 |
203 | Browsers:
204 | A browser lets you browse your messages, with a preview of date, sender(s) and subject.
205 | Galore comes with 3 browsers depending on your need/preference.
206 | Message browser: Display one message at the time, sorted by date.
207 | Thread browser: Display a thread as one entry, sorted by when the thread resieved it's last message.
208 | Thread message browser: Display messages but group them as threads, displaying a treelike structure.
209 |
210 | View:
211 | Views emails and comes in 2 flavours: thread viewer and message viewer.
212 | Thread viewer: View all messages in a thread. Action taken depends on the cursor location.
213 | Message view: View a single message.
214 |
215 | Compose:
216 | Write an email, send it, store as a draft or discard it. Compared to mutt, attachments are added
217 | while editing the email (or when generating the response) and displayed as virtual lines at the bottom
218 | of the text. Compose also allows hidden headers (see compose_headers in config),
219 | to unclutter your mailing experiance.
220 |
221 | Tips and trix
222 | -------------
223 | You want a small gpg-ui to complement the email client?
224 | That is easy, with a plugin like toggleterm and gpg-tui, we can make
225 | a small popup window to manage your keys from
226 | ``` lua
227 | local terms = require("toggleterm.terminal")
228 | local gpgtui = terms.Terminal:new({
229 | cmd = "gpg-tui",
230 | direction = "float",
231 | float_opts = {
232 | border = "single",
233 | },
234 | })
235 |
236 | local function gpgtui_toggle()
237 | gpgtui:toggle()
238 | end
239 |
240 | vim.keymap.set('n', 'mg', gpgtui_toggle, {noremap = true, silent = true})
241 | ```
242 |
243 | Look in examples for more ways extend galore for your needs.
244 |
--------------------------------------------------------------------------------
/autoload/health/galore.vim:
--------------------------------------------------------------------------------
1 | function! health#galore#check()
2 | lua require 'galore.health'.check()
3 | endfunction
4 |
--------------------------------------------------------------------------------
/doc/galore.txt:
--------------------------------------------------------------------------------
1 | *nvim-galore* *galore*
2 |
3 | A notmuch email client for neovim
4 |
5 | ==============================================================================
6 | *galore-contents*
7 |
8 | Abstract |galore-abstract|
9 | Usage |galore-usage|
10 | Configure |galore-configure|
11 | Functions |galore-functions|
12 |
13 | ==============================================================================
14 | Abstract *galore-abstract*
15 | An email client with the power of neovim!
16 |
17 | ==============================================================================
18 | Usage *galore-usage*
19 |
20 | To use, you need to have notmuch installed and configured:
21 |
22 | To use all features of galore, install the following:
23 | use {'
24 | dagle/galore', run = 'make',
25 | requires = {
26 | 'nvim-telescope/telescope.nvim',
27 | 'nvim-lua/popup.nvim',
28 | 'nvim-lua/plenary.nvim',
29 | 'nvim-telescope/telescope-file-browser.nvim',
30 | 'hrsh7th/nvim-cmp',
31 | }
32 | }
33 |
34 | It also has optional support for the address book mates, so in stall that for
35 | mates support
36 |
37 | Then to setup galore:
38 | local galore = require('galore')
39 | galore.setup()
40 |
41 | To start galore:
42 | galore.open()
43 | or
44 | :Galore
45 |
46 | ==============================================================================
47 | Configure *galore-config*
48 | For settings to change, look in lua/galore/config.lua
49 | for values to change with setup.
50 | They will be documented here in the future.
51 |
52 | For cmp support you need to add the following:
53 | { name = 'vcard_addr'},
54 | { name = 'notmuch_addr'},
55 | to your cmp sources
56 |
57 | ==============================================================================
58 | Global functions *Global functions*
59 |
60 | Most of galore functionallity can be ran globaly, from anywhere but you want to make sure
61 | that libs are loaded (they are lazy loaded by default).
62 |
63 | Here are 2 examples:
64 | vim.keymap.set('n', 'mf', function()
65 | require("galore").withconnect(function ()
66 | require("galore.telescope").load_draft() end)
67 | end, {desc='Load draft'})
68 |
69 | vim.keymap.set('n', 'mf', function()
70 | require("galore").withconnect(function ()
71 | require("galore.thread_message_browser"):create("tag:work", {}) end)
72 | end, {desc='Open work inbox'})
73 |
74 | ==============================================================================
75 | Buffers *galore-buffers*
76 |
77 | Galore comes with a couple of different buffers to navigate your email
78 |
79 | Saved is a buffer for saved searches, selecting a saved search will run that search
80 | in a selected browsers. Saved accepts a list of generators that produce output.
81 | gen_tags: Take all tags the db uses and display them one by one.
82 | gen_internal: All searches we have saved internally and
83 | don't want to export into the ecosystem (saved in a file)
84 | gen_excluded: tags excluded from our searches. For example if we exclude archive,
85 | then archive will gets it's own entry
86 |
87 | Browsers:
88 | A browser lets you browse your messages, with a preview of date, sender(s) and subject.
89 | Galore comes with 3 browsers depending on your need/preference.
90 | Message browser: Display one message at the time, sorted by date.
91 | Thread browser: Display a thread as one entry, sorted by when the thread resieved it's last message.
92 | Thread message browser: Display messages but group them as threads, displaying a treelike structure.
93 |
94 | View:
95 | Views emails and comes in 2 flavours: thread viewer and message viewer.
96 | Thread viewer: View all messages in a thread. Action taken depends on the cursor location.
97 | Message view: View a single message.
98 |
99 | Compose:
100 | Write an email, send it, store as a draft or discard it. Compared to mutt, attachments are added
101 | while editing the email (or when generating the response) and displayed as virtual lines at the bottom
102 | of the text. Compose also allows hidden headers (see compose_headers in config),
103 | to unclutter your mailing experiance.
104 |
--------------------------------------------------------------------------------
/doc/tags:
--------------------------------------------------------------------------------
1 | galore galore.txt /*galore*
2 | galore-abstract galore.txt /*galore-abstract*
3 | galore-buffers galore.txt /*galore-buffers*
4 | galore-config galore.txt /*galore-config*
5 | galore-contents galore.txt /*galore-contents*
6 | galore-usage galore.txt /*galore-usage*
7 | nvim-galore galore.txt /*nvim-galore*
8 |
--------------------------------------------------------------------------------
/examples/gpg-tui.lua:
--------------------------------------------------------------------------------
1 | local terms = require("toggleterm.terminal")
2 | local gpgtui = terms.Terminal:new({
3 | cmd = "gpg-tui",
4 | direction = "float",
5 | float_opts = {
6 | border = "single",
7 | },
8 | })
9 |
10 | local function gpgtui_toggle()
11 | gpgtui:toggle()
12 | end
13 |
14 | vim.keymap.set('n', 'mg', gpgtui_toggle, {noremap = true, silent = true})
15 |
--------------------------------------------------------------------------------
/examples/keybindings.lua:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dagle/galore/0fe4c74186d18e6449b1be951564c5392cf970d2/examples/keybindings.lua
--------------------------------------------------------------------------------
/examples/khard.lua:
--------------------------------------------------------------------------------
1 | local terms = require("toggleterm.terminal")
2 | local gpgtui = terms.Terminal:new({
3 | cmd = "khard add",
4 | direction = "float",
5 | float_opts = {
6 | border = "single",
7 | },
8 | })
9 |
10 | local function gpgtui_toggle()
11 | gpgtui:toggle()
12 | end
13 |
14 | vim.keymap.set('n', 'mk', gpgtui_toggle, {noremap = true, silent = true})
15 |
--------------------------------------------------------------------------------
/examples/snippets.lua:
--------------------------------------------------------------------------------
1 | --- A small example on how to use luasnip
2 | --- for email. The filetype filetype that compose uses is mail
3 | local ls = require("luasnip")
4 | local s = ls.snippet
5 | local t = ls.text_node
6 |
7 | local function replace_text(replace)
8 | if type(replace) == "string" then
9 | return replace
10 | end
11 | if type(replace) == "table" then
12 | if replace.group then
13 | local ret = replace.group .. ": "
14 | ret = ret .. table.concat(replace, " ") .. ";"
15 | return ret
16 | end
17 | return table.concat(replace, " ")
18 | end
19 | end
20 |
21 | local alias = {{"Work-emails", "example@example.org, test@test.org"}}
22 |
23 | for _, v in ipairs(alias) do
24 | local text = replace_text(v[2])
25 | ls.add_snippets("mail", {
26 | s(v[1], {
27 | t({text})
28 | })
29 | })
30 | end
31 |
--------------------------------------------------------------------------------
/galore.desktop:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Name=Galore Mail
3 | Comment=A neovim email client using notmuch
4 | Exec=nvim -c ":GaloreCompose %u"
5 | Icon=galore
6 | Terminal=true
7 | Type=Application
8 | Keywords=Email;E-mail;
9 | Categories=Network;Email;
10 | MimeType=x-scheme-handler/mailto;
11 |
--------------------------------------------------------------------------------
/install_local.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # If we want to install it globally, you don't want to run this file
3 |
4 | NAME=$(dirname "$0")
5 | BASEDIR=$(realpath "${NAME}")
6 |
7 | if [ -d "${BASEDIR}/build" ]; then
8 | rm -r "${BASEDIR}"/build
9 | fi
10 | meson "${BASEDIR}"/build
11 | meson install -C "${BASEDIR}"/build
12 | sed -i "s|libgalore.so|${BASEDIR}/build/src/libgalore.so|" "${BASEDIR}"/build/src/Galore-0.1.gir
13 | g-ir-compiler "${BASEDIR}"/build/src/Galore-0.1.gir --output "${BASEDIR}"/build/src/Galore-0.1.typelib
14 |
--------------------------------------------------------------------------------
/lua/galore/address-util.lua:
--------------------------------------------------------------------------------
1 | local config = require('galore.config')
2 | local gu = require('galore.gmime-util')
3 | local lgi = require('lgi')
4 | local gmime = lgi.require('GMime', '3.0')
5 |
6 | local M = {}
7 |
8 | local function unwild(ia)
9 | -- TODO
10 | -- remove + and - from the local part of the email
11 | -- so xyz+spam@domain.com => xyz@domain.com
12 | return ia
13 | end
14 |
15 | -- TODO expose
16 | local function normalize(ia1)
17 | if gmime.InternetAddressMailbox:is_type_of(ia1) then
18 | return ia1:get_idn_addr()
19 | end
20 | return ia1:to_string()
21 | end
22 |
23 | -- TODO expose
24 | local function removetags(emailstr)
25 | return emailstr.gsub('[+-].-@', '@')
26 | end
27 |
28 | function M.address_equal(ia1, ia2)
29 | -- we don't support groups for now
30 | local e1 = normalize(ia1)
31 | local e2 = normalize(ia2)
32 | return unwild(e1) == unwild(e2)
33 | end
34 |
35 | --- TODO move all compare functions to it's own file
36 | function M.ialist_contains(ia2, ialist)
37 | for ia1 in gu.internet_address_list_iter(ialist) do
38 | if M.address_equal(ia1, ia2) then
39 | return true
40 | end
41 | end
42 | return false
43 | end
44 |
45 | local function search_for(received, ourlist)
46 | local match, num = string.gsub(received, '.*by [^@]*@([^ >]*).*', '%1')
47 | if num > 0 then
48 | for _, mail in gu.internet_address_list_iter(ourlist) do
49 | local e = normalize(mail)
50 | -- TODO match might not be idn normalized
51 | if e:find(match) then
52 | return e
53 | end
54 | end
55 | end
56 | end
57 |
58 | local function search_by(received, ourlist)
59 | local match, num = string.gsub(received, '.*for [^@]*@([^ >]*).*', '%1')
60 | if num > 0 then
61 | for _, mail in gu.internet_address_list_iter(ourlist) do
62 | local e = normalize(mail)
63 | -- TODO match might not be idn normalized
64 | if e:find(match) then
65 | return e
66 | end
67 | end
68 | end
69 | end
70 |
71 | --- TODO
72 | local function guess_from(message, ourlist)
73 | -- local received = M.header_get_values(object, "Received")
74 | -- for _, r in ipairs(received) do
75 | -- search_for(r)
76 | -- end
77 | -- for _, r in ipairs(received) do
78 | -- search_by(r)
79 | -- end
80 | return nil
81 | end
82 |
83 | --- get what email addr we used to recieve this email
84 | --- useful if you have multiple emails and want to respond
85 | --- from the correct email
86 | --- This looks horrible and is but it's kinda tho only way
87 | --- and how is most mailers do it
88 | function M.get_our_email(message)
89 | local at = gmime.AddressType
90 | local emails = {}
91 | table.insert(emails, config.values.primary_email)
92 | vim.list_extend(emails, config.values.other_email)
93 | local str = table.concat(emails, ', ')
94 | local ourlist = gmime.InternetAddressList.parse(nil, str)
95 | local normal = {
96 | at.TO,
97 | at.CC,
98 | at.BCC,
99 | }
100 | local extra = {
101 | 'Envelope-to',
102 | 'X-Original-To',
103 | 'Delivered-To',
104 | }
105 | for _, h in ipairs(normal) do
106 | local addr = message:get_addresses(h)
107 | if not addr then
108 | goto continue
109 | end
110 | for ia1 in gu.internet_address_list_iter(addr) do
111 | if M.ialist_contains(ia1, ourlist) then
112 | return ia1
113 | end
114 | end
115 | ::continue::
116 | end
117 | for _, h in ipairs(extra) do
118 | local header = message:get_header(h)
119 | if not header then
120 | goto continue
121 | end
122 |
123 | for ia1 in gu.internet_address_list_iter_str(header, nil) do
124 | if M.ialist_contains(ia1, ourlist) then
125 | return ia1
126 | end
127 | end
128 | ::continue::
129 | end
130 |
131 | if not config.values.guess_email then
132 | return config.values.primary_email
133 | end
134 | local guess = guess_from(message, ourlist)
135 | if guess then
136 | return guess
137 | end
138 | return config.values.primary_email
139 | end
140 |
141 | return M
142 |
--------------------------------------------------------------------------------
/lua/galore/browser.lua:
--------------------------------------------------------------------------------
1 | local async_job = require('telescope._')
2 | local LinesPipe = async_job.LinesPipe
3 | local async = require('plenary.async')
4 | local Browser = {}
5 |
6 | --- Move to the next line in the browser
7 | function Browser.next(browser, line)
8 | line = math.min(line + 1, #browser.State)
9 | local line_info = browser.State[line]
10 | return line_info, line
11 | end
12 |
13 | --- Move to the next line in the browser
14 | function Browser.prev(browser, line)
15 | line = math.max(line - 1, 1)
16 | local line_info = browser.State[line]
17 | return line_info, line
18 | end
19 |
20 | --- Move to the prev line in the browser
21 | function Browser.select(browser)
22 | local line = vim.api.nvim_win_get_cursor(0)[1]
23 | return line, browser.State[line]
24 | end
25 |
26 | --- Commit the line to the browser
27 | --- Moving to the next line doesn't really move to the next line
28 | --- it asks for the next line relative to the line in the view
29 | --- This move the line for all windows that has the browser buffer
30 | function Browser.set_line(browser, line)
31 | local wins = vim.fn.win_findbuf(browser.handle)
32 | for _, win in ipairs(wins) do
33 | local location = vim.api.nvim_win_get_cursor(win)
34 | vim.api.nvim_win_set_cursor(win, { line, location[2] })
35 | end
36 | end
37 |
38 | function Browser.update_lines_helper(self, mode, search, line_nr)
39 | local bufnr = self.handle
40 | local args = { 'nm-livesearch', '-d', self.opts.runtime.db_path, mode, search }
41 | if self.opts.show_message_description then
42 | args = {
43 | 'nm-livesearch',
44 | '-e',
45 | self.opts.show_message_description[1],
46 | '-r',
47 | self.opts.show_message_description[2],
48 | '-d',
49 | self.opts.runtime.db_path,
50 | mode,
51 | search,
52 | }
53 | end
54 | vim.fn.jobstart(args, {
55 | stdout_buffered = true,
56 | stderr_buffered = true,
57 | on_stderr = function(_, data, _)
58 | vim.api.nvim_err_write(string.format("Couldn't update line for %s and %d", search, line_nr))
59 | end,
60 | on_stdout = function(_, data, _)
61 | local message = vim.fn.json_decode(data)
62 | if message then
63 | vim.api.nvim_buf_set_option(bufnr, 'readonly', false)
64 | vim.api.nvim_buf_set_option(bufnr, 'modifiable', true)
65 | vim.api.nvim_buf_set_lines(bufnr, line_nr - 1, line_nr, false, { message.entry })
66 | if message.highlight then
67 | vim.api.nvim_buf_add_highlight(bufnr, self.dians, 'GaloreHighlight', line_nr, 0, -1)
68 | end
69 | vim.api.nvim_buf_set_option(bufnr, 'readonly', true)
70 | vim.api.nvim_buf_set_option(bufnr, 'modifiable', false)
71 | end
72 | end,
73 | })
74 | end
75 |
76 | function Browser.get_entries(self, mode, buffer_runner)
77 | local stdout = LinesPipe()
78 | local writer = nil
79 |
80 | self.State = {}
81 |
82 | local job
83 | local iter
84 | local args = { '-d', self.opts.runtime.db_path, mode, self.search }
85 | if self.opts.show_message_description then
86 | args = vim.list_extend(
87 | { '-e', self.opts.show_message_description[1], '-r', self.opts.show_message_description[2] },
88 | args
89 | )
90 | end
91 | if self.opts.emph then
92 | local highlight = vim.json.encode(self.opts.emph)
93 | args = vim.list_extend({ '-h', highlight }, args)
94 | end
95 |
96 | job = async_job.spawn({
97 | command = 'nm-livesearch',
98 | args = args,
99 | writer = writer,
100 |
101 | stdout = stdout,
102 | })
103 | iter = stdout:iter(true)
104 | return setmetatable({
105 | close = function()
106 | if job then
107 | job:close(true)
108 | end
109 | iter = nil -- ?
110 | self.runner = nil
111 | end,
112 | resume = function(numlocal)
113 | if job and iter then
114 | local i = 1
115 |
116 | for line in iter do
117 | local entry = vim.json.decode(line)
118 | i = i + buffer_runner(entry, i)
119 | if numlocal and i > numlocal then
120 | return
121 | end
122 | end
123 | -- idk if I need these first 2
124 | job:close(true)
125 | iter = nil
126 | self.runner = nil
127 | end
128 | end,
129 | }, {
130 | -- __call = callable,
131 | })
132 | end
133 |
134 | -- Produce more entries when we scroll
135 | function Browser.scroll(self)
136 | vim.api.nvim_create_autocmd({ 'CursorMoved, WinScrolled' }, {
137 | buffer = self.handle,
138 | callback = function()
139 | local line = vim.api.nvim_win_get_cursor(0)[1]
140 | local nums = vim.api.nvim_buf_line_count(self.handle)
141 | local winsize = vim.api.nvim_win_get_height(0)
142 | local treshold = winsize * 4
143 | if (nums - line) < treshold then
144 | -- We should make sure that we are alone!
145 | if self.runner then
146 | if not self.updating then
147 | self.updating = true
148 | local func = async.void(function()
149 | self:unlock()
150 | self.runner.resume(math.max(treshold, self.opts.limit))
151 | self:lock()
152 | self.updating = false
153 | end)
154 | func()
155 | end
156 | end
157 | end
158 | end,
159 | })
160 | end
161 |
162 | local function find_highlight_helper(highlight, start, stop, pos)
163 | local mid = math.floor((start + stop) / 2)
164 | local item = highlight[mid]
165 | if pos == item then
166 | return mid
167 | elseif start == stop then
168 | if pos < item then
169 | return start - 1
170 | else
171 | return start
172 | end
173 | elseif pos > item then
174 | return find_highlight_helper(highlight, math.min(mid + 1, stop), stop, pos)
175 | else
176 | return find_highlight_helper(highlight, start, math.max(mid - 1, start), pos)
177 | end
178 | end
179 |
180 | local function insert_highlight(highlight, pos)
181 | if pos < highlight[1] then
182 | table.insert(highlight, pos)
183 | return
184 | elseif pos > highlight[#highlight] then
185 | table.insert(highlight, #highlight, pos)
186 | return
187 | end
188 | local i = find_highlight_helper(highlight, 1, #highlight, pos, false)
189 | table.insert(highlight, i, pos)
190 | end
191 |
192 | local function find_highlight(highlight, pos)
193 | if pos < highlight[1] or pos > highlight[#highlight] then
194 | return nil
195 | end
196 |
197 | local index = find_highlight_helper(highlight, 1, #highlight, pos)
198 | if highlight[index] ~= pos then
199 | return nil
200 | end
201 | return index
202 | end
203 |
204 | local function highlight_move(highlight, win_id, pos, forward)
205 | local vert_pos = pos[1] - 1
206 | if not highlight or vim.tbl_isempty(highlight) then
207 | return
208 | end
209 |
210 | -- if vert_pos < highlight[1] or vert_pos > highlight[#highlight] then
211 | -- return nil
212 | -- end
213 |
214 | local index = find_highlight_helper(highlight, 1, #highlight, vert_pos)
215 |
216 | if not forward and index > 1 then
217 | vim.api.nvim_win_set_cursor(win_id, { highlight[index - 1] + 1, pos[2] })
218 | elseif forward and index < #highlight then
219 | vim.api.nvim_win_set_cursor(win_id, { highlight[index + 1] + 1, pos[2] })
220 | end
221 | end
222 |
223 | function Browser.prev_highlight(browser)
224 | local win_id = vim.api.nvim_get_current_win()
225 | local pos = vim.api.nvim_win_get_cursor(win_id)
226 |
227 | highlight_move(browser.highlight, win_id, pos, false)
228 | end
229 |
230 | function Browser.next_highlight(browser)
231 | local win_id = vim.api.nvim_get_current_win()
232 | -- local bufnr = vim.api.nvim_win_get_buf(win_id)
233 | local pos = vim.api.nvim_win_get_cursor(win_id)
234 |
235 | highlight_move(browser.highlight, win_id, pos, true)
236 | end
237 |
238 | return Browser
239 |
--------------------------------------------------------------------------------
/lua/galore/builder.lua:
--------------------------------------------------------------------------------
1 | local lgi = require('lgi')
2 | local gmime = lgi.require('GMime', '3.0')
3 |
4 | local gu = require('galore.gmime-util')
5 | local gcu = require('galore.crypt-utils')
6 | local u = require('galore.util')
7 | local runtime = require('galore.runtime')
8 | local log = require('galore.log')
9 |
10 | local M = {}
11 |
12 | local function make_attach(attach)
13 | local mime = u.collect(string.gmatch(attach.mime_type, '([^/]+)'))
14 | if #mime ~= nil then
15 | log.log_err('bad mime-type')
16 | end
17 | local attachment = gmime.Part.new_with_type(mime[1], mime[2])
18 | attachment:set_filename(attach.filename)
19 | return attachment
20 | end
21 |
22 | local function create_path_attachment(attach)
23 | local attachment = make_attach(attach)
24 |
25 | local fd = assert(vim.loop.fs_open(attach.path, 'r', 0644))
26 | local stream = gmime.StreamFs.new(fd)
27 |
28 | local content = gmime.DataWrapper.new_with_stream(stream, gmime.ContentEncoding.DEFAULT)
29 | attachment:set_content(content)
30 | return attachment
31 | end
32 |
33 | local function create_part_attachment(attach)
34 | local attachment = make_attach(attach)
35 |
36 | local content = attach.part:get_content()
37 | attachment:set_content(content)
38 | return attachment
39 | end
40 |
41 | local function create_data_attachment(attach)
42 | local attachment = make_attach(attach)
43 |
44 | local stream = gmime.StreamMem.new()
45 |
46 | local content = gmime.DataWrapper.new_with_stream(stream, gmime.ContentEncoding.DEFAULT)
47 | attachment:set_content(content)
48 | stream:write(stream, attach.data)
49 | stream:flush()
50 | return attachment
51 | end
52 |
53 | -- @param attach attachment
54 | -- @return A MimePart object containing an attachment
55 | -- Support encryption?
56 | local function create_attachment(attach)
57 | if attach.data then
58 | return create_data_attachment(attach)
59 | elseif attach.part then
60 | return create_part_attachment(attach)
61 | elseif attach.path then
62 | return create_path_attachment(attach)
63 | else
64 | return nil
65 | end
66 | end
67 |
68 | -- encrypt a part and return the multipart
69 | function M.secure(part, opts, recipients)
70 | local ctx = gmime.GpgContext.new()
71 | if opts.encrypt then
72 | local encrypt, err = gmime.MultipartEncrypted.encrypt(
73 | ctx,
74 | part,
75 | opts.sign,
76 | opts.gpg_id,
77 | opts.encrypt_flags,
78 | recipients
79 | )
80 | if encrypt ~= nil then
81 | return encrypt
82 | end
83 | if opts.encrypt == 2 then
84 | local str = string.format("Couldn't encrypt message: %s", err)
85 | log.log_err(str)
86 | end
87 | end
88 | if opts.sign then
89 | local signed, err = gcu.sign(ctx, part)
90 | if signed ~= nil then
91 | return signed
92 | else
93 | local str = string.format('Could not sign message: %s', err)
94 | log.log_err(str)
95 | end
96 | end
97 | -- if we don't want to sign and failed to encrypt
98 | -- we just return the part as is
99 | return part
100 | end
101 |
102 | local function required_headers(message)
103 | local function l(list)
104 | return list:length(list) > 0
105 | end
106 | local from = message:get_from()
107 | local to = message:get_to()
108 | local sub = message:get_subject()
109 | return l(from) and l(to) and sub and sub ~= ''
110 | end
111 |
112 | function M.textbuilder(text)
113 | local body = gmime.TextPart.new_with_subtype('plain')
114 | body:set_text(table.concat(text, '\n'))
115 | return body
116 | end
117 |
118 | -- create a message from strings
119 | -- @param buf table parameters:
120 | -- headers: headers to set, address_headers are parsed and concated
121 | -- body: A body to send to bodybuilder
122 | -- @param opts table. Accept these values:
123 | -- mid: string if you don't want an autogenerated message-id.
124 | -- encrypt: bool
125 | -- sign: bool
126 | -- gpg_id: id of your gpg-key, required if we want to sign the email
127 | -- encrypt_flags: "none"|"session"|"noverify"|"keyserver"|"online"
128 | -- @param attachments list of table. See attachment format
129 | -- @param extra_headers list headers that we set before buf (can be overwritten)
130 | -- @param bodybuilder function(any) string
131 | -- @return a gmime message
132 | function M.create_message(buf, opts, attachments, extra_headers, builder)
133 | local at = gmime.AddressType
134 | extra_headers = extra_headers or {}
135 | opts = opts or {}
136 | attachments = attachments or {}
137 | local current -- our current position in the mime tree
138 |
139 | local message = gmime.Message.new(true)
140 | local address_headers = { at.FROM, at.TO, at.CC, at.BCC, at.REPLY_TO } -- etc
141 |
142 | for k, v in pairs(extra_headers) do
143 | -- is "" == null and we use default charset?
144 | message:set_header(k, v, '')
145 | end
146 |
147 | for k, v in pairs(buf.headers) do
148 | k = k:lower()
149 | if vim.tbl_contains(address_headers, k) then
150 | local list = gmime.InternetAddress.parse(nil, v)
151 | -- TODO change k to enum
152 | local address = message:get_addresses(k)
153 | if not list then
154 | local err = string.format('Failed to parse %s-address:\n%s', v, buf.headers[v])
155 | log.error(err)
156 | return
157 | end
158 | address:append(list)
159 | else
160 | message:set_header(k, v, '')
161 | end
162 | end
163 |
164 | if opts.mid then
165 | message:set_message_id(message, opts.mid)
166 | else
167 | local id = gu.make_id(buf.headers.from)
168 | message:set_message_id(id)
169 | end
170 |
171 | gu.insert_current_date(message)
172 |
173 | message:set_subject(buf.headers.subject, '')
174 |
175 | if not required_headers(message) then
176 | log.error('Missing non-optinal headers')
177 | return
178 | end
179 | -- done with headers
180 |
181 | -- make a body
182 | current = builder(buf.body)
183 |
184 | if not vim.tbl_isempty(attachments) then
185 | local multipart = gmime.Multipart.new_with_subtype('mixed')
186 | multipart:add(current)
187 | current = multipart
188 | for _, attach in pairs(attachments) do
189 | local attachment = create_attachment(attach)
190 | if attachment then
191 | multipart:add(attachment)
192 | end
193 | end
194 | end
195 |
196 | if opts.encrypt or opts.sign then
197 | local function normalize(ia1)
198 | if gmime.InternetAddress:is_type_of(ia1) then
199 | return ia1:get_addr()
200 | end
201 | return ia1:get_name()
202 | end
203 | local recipients = {}
204 | local rec = message:get_all_recipients()
205 | for ia in gu.internet_address_list_iter(rec) do
206 | local norm = normalize(ia)
207 | table.insert(recipients, norm)
208 | end
209 | local secure = M.secure(current, opts, recipients)
210 | if secure then
211 | current = secure
212 | end
213 | end
214 |
215 | message:set_mime_part(current)
216 |
217 | return message
218 | end
219 |
220 | return M
221 |
--------------------------------------------------------------------------------
/lua/galore/callback.lua:
--------------------------------------------------------------------------------
1 | --- TODO, this file includes to much globally
2 | local message_view = require('galore.message_view')
3 | local thread_view = require('galore.thread_view')
4 | local compose = require('galore.compose')
5 | local gu = require('galore.gmime-util')
6 | local nu = require('galore.notmuch-util')
7 | local runtime = require('galore.runtime')
8 | local config = require('galore.config')
9 | local br = require('galore.browser')
10 | local tm = require('galore.templates')
11 | -- local nm = require("galore.notmuch")
12 | local nm = require('notmuch')
13 |
14 | local M = {}
15 |
16 | --- Select a saved search and open it in specified browser and mode
17 | --- @param saved any
18 | --- @param browser any
19 | --- @param mode any
20 | function M.select_search(saved, browser, mode)
21 | local search = saved:select()[4]
22 | browser:create(search, { kind = mode, parent = saved })
23 | end
24 |
25 | function M.select_search_default(saved, mode)
26 | local default_browser = saved.opts.default_browser or 'tmb'
27 | local browser
28 | if default_browser == 'tmb' then
29 | browser = require('galore.thread_message_browser')
30 | elseif default_browser == 'message' then
31 | browser = require('galore.message_browser')
32 | elseif default_browser == 'thread' then
33 | browser = require('galore.thread_browser')
34 | end
35 | M.select_search(saved, browser, mode)
36 | end
37 |
38 | --- Open the selected mail in the browser for viewing
39 | --- @param browser any
40 | --- @param mode any
41 | function M.select_message(browser, mode)
42 | local vline, line_info = br.select(browser)
43 | message_view:create(line_info, { kind = mode, parent = browser, vline = vline })
44 | end
45 |
46 | --- Open the selected thread in the browser for viewing
47 | --- @param browser any
48 | --- @param mode any
49 | function M.select_thread(browser, mode)
50 | local vline, line_info = browser:select_thread()
51 | thread_view:create(line_info, { kind = mode, parent = browser, vline = vline })
52 | end
53 |
54 | --- Yank the current line using the selector
55 | --- @param browser any
56 | function M.yank_browser(browser)
57 | local _, id = br.select(browser)
58 | vim.fn.setreg('', id)
59 | end
60 |
61 | --- Yank the current message using the selector
62 | --- @param mv any
63 | --- @param select any
64 | --- TODO change, we only save mid!
65 | function M.yank_message(mv, select)
66 | vim.fn.setreg('', mv.line[select])
67 | end
68 |
69 | function M.new_message(kind, opts)
70 | opts = opts or {}
71 | tm.compose_new(opts)
72 | compose:create(kind, opts)
73 | end
74 |
75 | --- load a message as it is
76 | function M.load_draft(kind, message, opts)
77 | opts = opts or {}
78 | tm.load_body(message, opts)
79 | tm.load_headers(message, opts)
80 | opts.mid = message:get_message_id(message)
81 | compose:create(kind, opts)
82 | end
83 |
84 | --- Create a reply compose from a message view
85 | --- @param message gmime.Message
86 | --- @param opts any
87 | function M.message_reply(kind, message, mode, opts)
88 | opts = opts or {}
89 | opts.reply = true
90 | mode = mode or 'reply'
91 | tm.load_body(message, opts)
92 | opts.Attach = nil
93 | tm.response_message(message, opts, mode)
94 | -- tm.response_message(message, opts, mode)
95 | gu.make_ref(message, opts)
96 | compose:create(kind, opts)
97 | end
98 |
99 | function M.send_template(opts)
100 | local buf = { headers = opts.headers, body = opts.Body }
101 | local send_opts = {}
102 | local message = builder.create_message(buf, send_opts, opts.Attach, {}, builder.textbuilder)
103 | -- something like this
104 | -- job.send_mail(message, function ()
105 | -- end
106 | end
107 |
108 | function M.mid_reply(kind, mid, mode, opts)
109 | local line
110 | opts = opts or {}
111 | runtime.with_db(function(db)
112 | local nm_message = nm.db_find_message(db, mid)
113 | line = nu.get_message(nm_message)
114 | end)
115 | local draft = vim.tbl_contains(line.tags, 'draft')
116 | local message = gu.parse_message(line.filenames[1])
117 | if message == nil then
118 | error("Couldn't parse message")
119 | end
120 | if draft then
121 | M.load_draft(kind, message, opts)
122 | else
123 | M.message_reply(kind, message, mode, opts)
124 | end
125 | end
126 |
127 | --- Change the tag for currently selected message and update the browser
128 | --- @param browser any
129 | --- @param tag string +tag to add tag, -tag to remove tag
130 | function M.change_tag(browser, tag)
131 | local vline, id = br.select(browser)
132 | if tag then
133 | runtime.with_db_writer(function(db)
134 | nu.change_tag(db, id, tag)
135 | nu.tag_if_nil(db, id, config.values.empty_tag)
136 | end)
137 | browser:update(vline)
138 | end
139 | end
140 |
141 | --- Ask for a change the tag for currently selected message and update the browser
142 | --- @param browser any
143 | function M.change_tag_ask(browser)
144 | vim.ui.input({ prompt = 'Tags change: ' }, function(tag)
145 | if tag then
146 | M.change_tag(browser, tag)
147 | else
148 | error('No tag')
149 | end
150 | end)
151 | end
152 |
153 | function M.change_tags_threads(tb, tag)
154 | local vline, thread = br.select(tb)
155 | runtime.with_db_writer(function(db)
156 | local q = nm.create_query(db, 'thread:' .. thread)
157 | for message in nm.thread_get_messages(thread) do
158 | local id = nm.message_get_id(message)
159 | nu.change_tag(db, id, tag)
160 | nu.tag_if_nil(db, id, config.value.empty_tag)
161 | end
162 | tb:update(vline)
163 | end)
164 | end
165 |
166 | --- Ask for a change the tag for currently selected message and update the browser
167 | --- @param tb any
168 | function M.change_tag_threads_ask(tb)
169 | vim.ui.input({ prompt = 'Tags change: ' }, function(tag)
170 | if tag then
171 | M.change_tags_threads(tb, tag)
172 | else
173 | error('No tag')
174 | end
175 | end)
176 | end
177 |
178 | return M
179 |
--------------------------------------------------------------------------------
/lua/galore/compose.lua:
--------------------------------------------------------------------------------
1 | local u = require('galore.util')
2 | local gu = require('galore.gmime-util')
3 | local ui = require('galore.ui')
4 | local Buffer = require('galore.lib.buffer')
5 | local job = require('galore.jobs')
6 | local builder = require('galore.builder')
7 | local nu = require('galore.notmuch-util')
8 | local runtime = require('galore.runtime')
9 | local Path = require('plenary.path')
10 | local debug = require('galore.debug')
11 | local o = require('galore.opts')
12 | local log = require('galore.log')
13 | -- local hd = require("galore.header-diagnostics")
14 |
15 | local Compose = Buffer:new()
16 |
17 | function Compose:add_attachment_path(file_path)
18 | local path = Path:new(file_path)
19 | if path:exists() and not path:is_file() then
20 | local filename = path:normalize()
21 | local mime_type = job.get_type(file_path)
22 | table.insert(self.attachments, { filename = filename, path = file_path, mime_type = mime_type })
23 | else
24 | log.log('Failed to add file', vim.log.levels.ERROR)
25 | end
26 | end
27 |
28 | function Compose:remove_attachment()
29 | vim.ui.select(self.attachments, { prompt = 'delete attachment' }, function(_, idx)
30 | if idx then
31 | table.remove(self.attachments, idx)
32 | end
33 | end)
34 | self:update_attachments()
35 | end
36 |
37 | function Compose:set_header_option(key, value)
38 | self.extra_headers[key] = value
39 | end
40 |
41 | function Compose:unset_option()
42 | local list = vim.tbl_keys(self.extra_headers)
43 | vim.ui.select(list, {
44 | prompt = 'Option to unset',
45 | format_item = function(item)
46 | return string.format('%s = %s', item, self.extra_headers[item])
47 | end,
48 | }, function(item, _)
49 | if item then
50 | self.extra_headers[item] = nil
51 | end
52 | end)
53 | end
54 |
55 | local function empty(str)
56 | return u.trim(str) == ''
57 | end
58 |
59 | --- TODO Test multiline!
60 | local function get_headers(self)
61 | local headers = {}
62 | local body_line = vim.api.nvim_buf_get_extmark_by_id(self.handle, self.compose, self.marks, {})[1]
63 | local lines = vim.api.nvim_buf_get_lines(self.handle, 0, body_line, true)
64 | local last_key
65 | for i, line in ipairs(lines) do
66 | if empty(line) then
67 | goto continue
68 | end
69 | local start, _, key, value = string.find(line, '^%s*(.-)%s*:(.+)')
70 | if start ~= nil and key ~= '' and value ~= '' then
71 | key = string.lower(key)
72 | last_key = key
73 | value = vim.fn.trim(value)
74 | headers[key] = value
75 | else
76 | if not last_key then
77 | -- this should be a log function
78 | local str = "Bad formated headers, trying to add to value header above that doesn't exist"
79 | log.log(str, vim.log.levels.ERROR)
80 | error(str)
81 | end
82 | -- local value, prev = unpack(headers[last_key])
83 | local key = headers[last_key]
84 | local extra = vim.fn.trim(line)
85 | -- headers[key] = {value .. " " .. extra, prev}
86 | headers[key] = value .. ' ' .. extra
87 | end
88 | ::continue::
89 | end
90 | return headers, body_line
91 | end
92 |
93 | function Compose:parse_buffer()
94 | local buf = {}
95 | local headers, body_line = get_headers(self)
96 | -- local lines = vim.api.nvim_buf_line_count(self.handle)
97 | local body = vim.api.nvim_buf_get_lines(self.handle, body_line + 1, -1, false)
98 | if headers.subject == nil then
99 | headers.subject = self.opts.empty_topic
100 | end
101 |
102 | -- for i = body_line + 1, lines do
103 | -- table.insert(body, lines[i])
104 | -- end
105 | buf.body = body
106 | buf.headers = headers
107 | return buf
108 | end
109 |
110 | -- TODO add opts
111 | function Compose:preview(kind)
112 | kind = kind or 'floating'
113 | local buf = self:parse_buffer()
114 |
115 | local message =
116 | builder.create_message(buf, self.opts, self.attachments, self.header_opts, builder.textbuilder)
117 | debug.view_raw_message(message, kind)
118 | self:set_option('modified', false)
119 | end
120 |
121 | -- Tries to send what is in the current buffer
122 | function Compose:send()
123 | -- local opts = {}
124 | local buf = self:parse_buffer()
125 | --- should be do encryt here or not?
126 |
127 | --- from here we want to be async
128 | local message = builder.create_message(
129 | buf,
130 | self.opts,
131 | self.attachments,
132 | self.extra_headers,
133 | builder.textbuilder
134 | )
135 | if not message then
136 | log.log("Couldn't create message for sending", vim.log.levels.ERROR)
137 | return
138 | end
139 | if self.opts.pre_sent_hooks then
140 | self.opts.pre_sent_hooks(message)
141 | end
142 |
143 | job.send_mail(message, function()
144 | log.log('Email sent', vim.log.levels.INFO)
145 | local reply = message:get_header('References')
146 | if reply then
147 | local mid = gu.unbracket(reply)
148 | runtime.with_db_writer(function(db)
149 | nu.change_tag(db, mid, '+replied')
150 | end)
151 | end
152 | --- add an option for this or move it to post_ehooks?
153 | job.insert_mail(message, self.opts.sent_dir, '+sent')
154 | if self.opts.post_sent_hooks then
155 | self.opts.post_sent_hooks(message)
156 | end
157 | end)
158 | self:set_option('modified', false)
159 | end
160 |
161 | function Compose:save_draft(build_opts)
162 | build_opts = build_opts or {}
163 | local buf = self:parse_buffer()
164 | if self.opts.draft_encrypt and self.opts.gpg_id then
165 | build_opts.encrypt = true
166 | build_opts.recipients = self.opts.gpg_id
167 | end
168 | local message = builder.create_message(
169 | buf,
170 | build_opts,
171 | self.attachments,
172 | self.extra_headers,
173 | builder.textbuilder
174 | )
175 | if not message then
176 | log.log("Couldn't create message for sending", vim.log.levels.ERROR)
177 | return
178 | end
179 | --- TODO from here we want to be async
180 | job.insert_mail(message, self.opts.draft_dir, self.opts.draft_tag)
181 | end
182 |
183 | function Compose:update_attachments()
184 | vim.api.nvim_buf_clear_namespace(self.handle, self.ns, 0, -1)
185 | local line = vim.fn.line('$') - 1
186 | ui.render_attachments(self.attachments, line, self.handle, self.ns)
187 | end
188 |
189 | function Compose:delete_tmp()
190 | -- vim.fn.delete()
191 | end
192 |
193 | local function addfiles(self, files)
194 | self.attachments = {}
195 | if not files then
196 | return
197 | end
198 | if type(files) == 'string' then
199 | self:add_attachment_path(files)
200 | return
201 | else
202 | self.attachments = files
203 | end
204 | end
205 |
206 | local function make_seperator(buffer, lines)
207 | local line_num = lines
208 | local col_num = 0
209 |
210 | local opts = {
211 | virt_lines = {
212 | { { 'Emailbody', 'GaloreSeperator' } },
213 | },
214 | }
215 | buffer.marks = buffer:set_extmark(buffer.compose, line_num, col_num, opts)
216 | end
217 |
218 | function Compose:commands()
219 | vim.api.nvim_buf_create_user_command(self.handle, 'GaloreAddAttachment', function(args)
220 | if args.fargs then
221 | for _, value in ipairs(args.fargs) do
222 | self:add_attachment(value)
223 | end
224 | self:update_attachments()
225 | end
226 | end, {
227 | nargs = '*',
228 | complete = 'file',
229 | })
230 | vim.api.nvim_buf_create_user_command(self.handle, 'GalorePreview', function()
231 | self:preview()
232 | end, {
233 | nargs = 0,
234 | })
235 | vim.api.nvim_buf_create_user_command(self.handle, 'GaloreSend', function()
236 | self:send()
237 | end, {
238 | nargs = 0,
239 | })
240 | end
241 |
242 | local function consume_headers(buffer)
243 | local lines = {}
244 | local extra_headers = {}
245 | local lookback = {}
246 | for _, v in ipairs(buffer.opts.compose_headers) do
247 | lookback[v[1]] = v[2]
248 | end
249 |
250 | for _, v in ipairs(buffer.opts.compose_headers) do
251 | if v[2] and not buffer.headers[v[1]] then
252 | local header = string.format('%s: ', v[1])
253 | table.insert(lines, header)
254 | elseif buffer.headers[v[1]] then
255 | local header = string.format('%s: %s', v[1], buffer.headers[v[1]])
256 | local multiline = vim.fn.split(header, '\n')
257 | vim.list_extend(lines, multiline)
258 | end
259 | end
260 |
261 | for k, v in pairs(buffer.headers) do
262 | if lookback[k] == nil then
263 | extra_headers[k] = v
264 | end
265 | end
266 | buffer.extra_headers = extra_headers
267 | buffer:set_lines(0, 0, true, lines)
268 | end
269 |
270 | local function render_body(buffer)
271 | if buffer.body then
272 | buffer:set_lines(-1, -1, true, buffer.body)
273 | end
274 | end
275 |
276 | function Compose:update()
277 | self:clear()
278 | consume_headers(self)
279 | local after = vim.fn.line('$') - 1
280 | render_body(self)
281 |
282 | make_seperator(self, after)
283 | end
284 |
285 | local function checkheaders(self)
286 | vim.api.nvim_create_autocmd('InsertLeave', {
287 | callback = function()
288 | local headers = get_headers(self)
289 | hd.checkheaders(self.dians, self.handle, headers)
290 | end,
291 | buffer = self.handle,
292 | })
293 | end
294 |
295 | -- change message to file
296 | function Compose:create(kind, opts)
297 | o.compose_options(opts)
298 | Buffer.create({
299 | name = opts.bufname(),
300 | ft = 'mail',
301 | kind = kind,
302 | cursor = 'top',
303 | parent = opts.parent,
304 | buftype = '', -- fix this
305 | modifiable = true,
306 | mappings = opts.key_bindings,
307 | init = function(buffer)
308 | buffer.opts = opts
309 | buffer.headers = opts.headers
310 | buffer.body = opts.Body
311 | addfiles(buffer, opts.Attach)
312 | buffer.ns = vim.api.nvim_create_namespace('galore-attachments')
313 | buffer.compose = vim.api.nvim_create_namespace('galore-compose')
314 | buffer.dians = vim.api.nvim_create_namespace('galore-dia')
315 | buffer:update()
316 | -- checkheaders(buffer)
317 |
318 | buffer:set_option('modified', false)
319 |
320 | buffer:update_attachments()
321 | opts.init(buffer)
322 | buffer:commands()
323 | end,
324 | }, Compose)
325 | end
326 |
327 | return Compose
328 |
--------------------------------------------------------------------------------
/lua/galore/crypt-utils.lua:
--------------------------------------------------------------------------------
1 | local config = require("galore.config")
2 | local lgi = require 'lgi'
3 | local log = require("galore.log")
4 | local gmime = lgi.require("GMime", "3.0")
5 |
6 | local M = {}
7 |
8 | local function verify_list(siglist)
9 | if siglist == nil then
10 | return false
11 | end
12 |
13 | local sigs = siglist:length(siglist) > 0
14 | -- P(siglist:length())
15 | for i = 0, siglist:length() do
16 | local sig = siglist:get_signature(i)
17 | if sig ~= nil then
18 | local test = config.values.validate_key(sig:get_status())
19 | sigs = sigs and test
20 | end
21 | end
22 | return sigs
23 | end
24 |
25 | function M.verify_signed(object)
26 | local signatures, error = object:verify(gmime.VerifyFlags(config.values.verify_flags))
27 | if not signatures and error then
28 | return false
29 | else
30 | return verify_list(signatures)
31 | end
32 | end
33 |
34 | function M.decrypt_and_verify(obj, flags, key)
35 | -- we don't get a result or do we get either a result or an error?
36 | local decrypted, result, err = obj:decrypt(
37 | flags,
38 | key
39 | )
40 |
41 | if not decrypted or err ~= nil then
42 | local str = string.format("Failed to decrypt message: %s", err)
43 | log.log(str, vim.log.levels.ERROR)
44 | return
45 | end
46 |
47 | local sign
48 | if result then
49 | sign = verify_list(result:get_signatures())
50 | end
51 |
52 | local new_key = result:get_session_key()
53 |
54 | return decrypted, sign, new_key
55 | end
56 |
57 | -- maybe not do these
58 | function M.sign(ctx, obj)
59 | local ret, err = gmime.MultipartSigned.sign(ctx, obj, config.gpg_id)
60 | return ret, err
61 | end
62 |
63 | -- maybe not do these
64 | function M.encrypt(ctx, obj, opts, recipients)
65 | local multi, err = gmime.MultipartEncrypted.encrypt(ctx, obj, opts.sign, opts.gpg_id, opts.encrypt_flags, recipients)
66 | return multi, err
67 | end
68 |
69 | return M
70 |
--------------------------------------------------------------------------------
/lua/galore/debug.lua:
--------------------------------------------------------------------------------
1 | local nm = require('notmuch')
2 | local runtime = require('galore.runtime')
3 | local Buffer = require('galore.lib.buffer')
4 | local gu = require('galore.gmime-util')
5 | local uv = vim.loop
6 |
7 | local lgi = require('lgi')
8 | local gmime = lgi.require('GMime', '3.0')
9 |
10 | local debug = {}
11 |
12 | --- Functions to aid in debugging.
13 | --- Atm it's just functions to view raw messages
14 | function debug.view_raw_file(filename, kind)
15 | Buffer.create({
16 | ft = 'mail',
17 | kind = kind or 'floating',
18 | cursor = 'top',
19 | init = function(buffer)
20 | local fd = assert(uv.fs_open(filename, 'r', 438))
21 | local stat = assert(uv.fs_fstat(fd))
22 | local data = assert(uv.fs_read(fd, stat.size, 0))
23 | assert(uv.fs_close(fd))
24 | data = vim.split(data, '\n')
25 | buffer:set_lines(0, 0, true, data)
26 | end,
27 | })
28 | end
29 |
30 | --- requires that the message is in the notmuch db
31 | function debug.view_raw_mid(mid, kind)
32 | local filename
33 | runtime.with_db(function(db)
34 | local message = nm.db_find_message(db, mid)
35 | filename = nm.message_get_filename(message)
36 | end)
37 | debug.view_raw_file(filename, kind)
38 | end
39 |
40 | --- maybe not do this for really big files? Maybe keep a cap?
41 | function debug.view_raw_attachment(attachment, kind)
42 | Buffer.create({
43 | ft = attachment.mime_type,
44 | kind = kind or 'floating',
45 | cursor = 'top',
46 | init = function(buffer)
47 | local buf
48 | if attachment.part then
49 | buf = gu.part_to_string(attachment.part)
50 | elseif attachment.data then
51 | buf = attachment.data
52 | elseif attachment.filename then
53 | local fd = assert(uv.fs_open(attachment.filename, 'r', 438))
54 | local stat = assert(uv.fs_fstat(fd))
55 | buf = assert(uv.fs_read(fd, stat.size, 0))
56 | assert(uv.fs_close(fd))
57 | end
58 | local fixed = vim.split(buf, '\n', false)
59 | buffer:set_lines(0, 0, true, fixed)
60 | end,
61 | })
62 | end
63 |
64 | function debug.view_raw_message(message, kind)
65 | if not message then
66 | return
67 | end
68 | local mem = gmime.StreamMem.new()
69 | message:write_to_stream(nil, mem)
70 | mem:flush()
71 | local str = mem:get_byte_array()
72 | local tbl = vim.split(str, '\n')
73 | Buffer.create({
74 | ft = 'mail',
75 | kind = kind or 'floating',
76 | cursor = 'top',
77 | init = function(buffer)
78 | buffer:set_lines(0, 0, true, tbl)
79 | end,
80 | })
81 | end
82 |
83 | return debug
84 |
--------------------------------------------------------------------------------
/lua/galore/default.lua:
--------------------------------------------------------------------------------
1 | local config = require('galore.config')
2 | local grouped = config.values.browser_grouped
3 | local M = {}
4 |
5 | function M.init(opts, searches)
6 | local saved = require('galore.saved')
7 | local group = vim.api.nvim_create_augroup('galore-windowstyle', { clear = true })
8 | vim.api.nvim_create_autocmd({ 'BufEnter', 'Filetype' }, {
9 | pattern = { 'galore-threads*', 'galore-messages' },
10 | group = group,
11 | callback = function()
12 | vim.api.nvim_win_set_option(0, 'foldlevel', 1)
13 | vim.api.nvim_win_set_option(0, 'foldmethod', 'manual')
14 | vim.api.nvim_win_set_option(0, 'foldcolumn', '1')
15 | end,
16 | })
17 | vim.api.nvim_create_autocmd({ 'BufEnter', 'Filetype' }, {
18 | pattern = { 'mail' },
19 | group = group,
20 | callback = function()
21 | vim.api.nvim_win_set_option(0, 'foldlevel', 99)
22 | vim.api.nvim_win_set_option(0, 'foldmethod', 'syntax')
23 | vim.api.nvim_win_set_option(0, 'foldcolumn', '1')
24 | end,
25 | })
26 | return saved:create(opts, searches)
27 | end
28 |
29 | return M
30 |
--------------------------------------------------------------------------------
/lua/galore/gmime-util.lua:
--------------------------------------------------------------------------------
1 | local config = require('galore.config')
2 | local lgi = require('lgi')
3 | local gmime = lgi.require('GMime', '3.0')
4 | local glib = lgi.require('GLib', '2.0')
5 |
6 | local M = {}
7 |
8 | function M.get_domainname(mail_str)
9 | local fqdn = mail_str:gsub('.*@(%w*)%.(%w*).*', '%1.%2')
10 | return fqdn
11 | end
12 |
13 | function M.insert_current_date(message)
14 | local time = os.time()
15 | local gtime = glib.DateTime.new_from_unix_local(time)
16 | message:set_date(gtime)
17 | end
18 |
19 | function M.make_id(from)
20 | local fdqn = M.get_domainname(from)
21 | return gmime.utils_generate_message_id(fdqn)
22 | end
23 |
24 | function M.mime_type(object)
25 | local ct = object:get_content_type()
26 | if ct then
27 | local type = ct:get_mime_type()
28 | return type
29 | end
30 | end
31 |
32 | function M.parse_message(filename)
33 | local stream = gmime.StreamFile.open(filename, 'r')
34 | local parser = gmime.Parser.new_with_stream(stream)
35 | local opts = gmime.ParserOptions.new()
36 | local message = parser:construct_message(opts)
37 | return message
38 | end
39 |
40 | --- tries to recorstruct a partial message,
41 | --- if the message isn't partial then just parse the
42 | --- message
43 | -- idx needs to be in bound
44 | function M.reconstruct(filenames, idx)
45 | if #filenames == 1 then
46 | return M.parse_message(filenames[idx])
47 | end
48 | local parts = {}
49 |
50 | local main_message = M.parse_message(filenames[idx])
51 | if main_message then
52 | local is_partial = false
53 | main_message:foreach(function(_, part)
54 | if gmime.MessagePartial:is_type_of(part) then
55 | is_partial = true
56 | local id = part:get_id(part)
57 | parts[id] = {}
58 | end
59 | end)
60 | if not is_partial then
61 | return main_message
62 | end
63 |
64 | for filename in ipairs(filenames) do
65 | local message = M.parse_message(filename)
66 | message:foreach(function(_, part)
67 | if gmime.MessagePartial:is_type_of(part) then
68 | local id = part:get_id(part)
69 | table.insert(parts[id], part)
70 | end
71 | end)
72 | end
73 | --- TODO
74 | --- we only return the first message of the partials
75 | for _, partial in pairs(parts) do
76 | if #partial == partial[1]:get_total() then
77 | local message = gmime.MessagePartial.reconstruct_message(partial)
78 | return message
79 | end
80 | end
81 | -- fall back to original message
82 | return main_message
83 | end
84 | end
85 |
86 | function M.make_ref(message, opts)
87 | local ref_str = message:get_header('References')
88 | local ref
89 | if ref_str then
90 | ref = gmime.References.parse(nil, ref_str)
91 | else
92 | ref = gmime.References.new()
93 | end
94 | local mid = message:get_message_id()
95 | local reply = gmime.References.parse(nil, mid)
96 | ref:append(mid)
97 | opts.headers = opts.headers or {}
98 | opts.headers.References = M.references_format(ref)
99 | opts.headers['In-Reply-To'] = M.references_format(reply)
100 | end
101 |
102 | function M.references_format(refs)
103 | if refs == nil then
104 | return nil
105 | end
106 | local box = {}
107 | for ref in M.reference_iter(refs) do
108 | table.insert(box, '<' .. ref .. '>')
109 | end
110 | return table.concat(box, '\n\t')
111 | end
112 |
113 | function M.is_multipart_alt(object)
114 | local type = M.mime_type(object)
115 | if type == 'multipart/alternative' then
116 | return true
117 | end
118 | return false
119 | end
120 |
121 | function M.is_multipart_multilingual(object)
122 | local type = M.mime_type(object)
123 | if type == 'multipart/alternative' then
124 | return true
125 | end
126 | return false
127 | end
128 |
129 | function M.is_multipart_related(object)
130 | local type = M.mime_type(object)
131 | if type == 'multipart/alternative' then
132 | return true
133 | end
134 | return false
135 | end
136 |
137 | function M.multipart_foreach_level(part, parent, fun, level)
138 | if parent ~= part then
139 | fun(parent, part, level)
140 | end
141 | if gmime.Multipart:is_type_of(part) then
142 | local i = 0
143 | local j = part:get_count()
144 | level = level + 1
145 | while i < j do
146 | local child = part:get_part(i)
147 | M.multipart_foreach_level(child, part, fun, level)
148 | i = i + 1
149 | end
150 | end
151 | end
152 |
153 | function M.message_foreach_level(message, fun)
154 | local level = 1
155 | if not message or not fun then
156 | return
157 | end
158 | local part = message:get_mime_part()
159 | fun(part, part, level)
160 |
161 | if gmime.Multipart:is_type_of(part) then
162 | M.multipart_foreach_level(part, part, fun, level)
163 | end
164 | end
165 |
166 | --- @param str string
167 | --- @param opts gmime.ParserOptions|nil
168 | --- @return function
169 | function M.reference_iter_str(str, opts)
170 | local refs = gmime.Reference.parse(opts, str)
171 | if refs == nil then
172 | return function()
173 | return nil
174 | end
175 | end
176 | return M.reference_iter(refs)
177 | end
178 |
179 | --- @return function
180 | function M.reference_iter(refs)
181 | local i = 0
182 | return function()
183 | if i < refs:length() then
184 | local ref = refs:get_message_id(i)
185 | i = i + 1
186 | return ref
187 | end
188 | end
189 | end
190 |
191 | function M.header_iter(object)
192 | local ls = object:get_header_list()
193 | if ls == nil then
194 | return function()
195 | return nil
196 | end
197 | end
198 | local j = ls:get_count()
199 | local i = 0
200 | return function()
201 | if i < j then
202 | local header = ls:get_header_at(i)
203 | if header == nil then
204 | return nil, nil
205 | end
206 | local key = header:get_name()
207 | local value = header:get_value()
208 | i = i + 1
209 | return key, value
210 | end
211 | end
212 | end
213 |
214 | function M.internet_address_list_iter_str(str, opt)
215 | local list = gmime.InternetAddressList.parse(opt, str)
216 | if list == nil then
217 | return function()
218 | return nil
219 | end
220 | end
221 | return M.internet_address_list_iter(list)
222 | end
223 |
224 | function M.internet_address_list_iter(list)
225 | local i = 0
226 | return function()
227 | if i < list:length() then
228 | local addr = list:get_address(i)
229 | i = i + 1
230 | return addr
231 | end
232 | end
233 | end
234 |
235 | return M
236 |
--------------------------------------------------------------------------------
/lua/galore/health.lua:
--------------------------------------------------------------------------------
1 | local M = {}
2 | local health = vim.health
3 |
4 | M.check = function()
5 | health.report_start('Galore health check')
6 | if vim.fn.executable('notmuch') == 1 then
7 | health.report_ok('Found notmuch')
8 | else
9 | health.report_error("Could not find notmuch, galore won't work")
10 | end
11 | if vim.fn.executable('nm-livesearch') == 1 then
12 | health.report_ok('found the executable nm-livesearch')
13 | else
14 | health.report_error("Could not find nm-livesearch, galore won't work")
15 | end
16 | if vim.fn.executable('browser-pipe') == 1 then
17 | health.report_ok('found the executable browser-pipe')
18 | else
19 | health.report_error(
20 | "Could not find browser-pipe, won't be able to view html in an external browser"
21 | )
22 | end
23 | if vim.fn.executable('file') == 1 then
24 | health.report_ok('found the find executable')
25 | else
26 | health.report_error("Could not find file, galore won't work")
27 | end
28 | if pcall(require, 'cmp') then
29 | health.report_ok('Found cmp')
30 | else
31 | health.report_info('Missing cmp for email completion')
32 | end
33 | if pcall(require, 'telescope') then
34 | health.report_ok('Found telescope')
35 | else
36 | health.report_info('Missing telescope for email search')
37 | end
38 | if vim.fn.executable('mates') == 1 then
39 | health.report_ok('Found mates')
40 | else
41 | health.report_info('Missing optional mates for vcard support')
42 | end
43 | if vim.fn.executable('w3m') == 1 then
44 | health.report_ok('found w3m')
45 | else
46 | health.report_info('Missing w3m, the default html render')
47 | end
48 | end
49 |
50 | return M
51 |
--------------------------------------------------------------------------------
/lua/galore/hooks.lua:
--------------------------------------------------------------------------------
1 | --- A hook takes a message and returns a boolean if success
2 | local config = require('galore.config')
3 | local job = require('galore.jobs')
4 |
5 | local lgi = require('lgi')
6 | local gmime = lgi.require('GMime', '3.0')
7 |
8 | local M = {}
9 |
10 | -- A pre hook is always return a boolean. If a pre hook return false
11 |
12 | --- @param message any
13 | --- @return boolean
14 | local function has_attachment(message)
15 | local state = {}
16 | local find_attachment = function(_, part, _)
17 | if gmime.Part:is_type_of(part) and part:is_attchment(part) then
18 | state.attachment = true
19 | end
20 | end
21 | message:foreach(find_attachment)
22 | return state.attachment
23 | end
24 |
25 | --- Pre sending, check for attachments
26 | --- @return boolean
27 | function M.missed_attachment(message)
28 | local sub = message:get_subject()
29 | local start, _ = string.find(sub, '[a,A]ttachment')
30 | local re, _ = string.find(sub, '^%s*Re:')
31 | if start and not re and not has_attachment(message) then
32 | return false
33 | end
34 | return true
35 | end
36 |
37 | --- Pre sending, ask if you want to send the email
38 | --- Should be in general be inserted before IO functions (like FCC)
39 | --- @return boolean
40 | function M.confirm()
41 | local ret = false
42 | vim.ui.input({
43 | prompt = 'Wanna send email? Y[es]',
44 | }, function(input)
45 | if input then
46 | input = input:lower()
47 | if input == 'yes' or 'y' then
48 | ret = true
49 | end
50 | end
51 | end)
52 | return ret
53 | end
54 |
55 | --- @return boolean
56 | function M.fcc_nm_insert(message)
57 | local fcc_str = message:get_header('FCC')
58 | message:remove_header('FCC')
59 |
60 | -- Should we try to guard users?
61 | -- this shouldn't be an abs path
62 | local path = config.values.fccdir .. '/' .. fcc_str
63 |
64 | job.insert_mail(message, path, '')
65 | end
66 |
67 | --- write file to a maildir
68 | --- this should do a lot more checks
69 | --- create dirs etc if it doesn't exist
70 |
71 | --- Pre send, insert the mail into fcc dir and then remove FCC header
72 | --- @param message
73 | --- @return boolean
74 | function M.fcc_fs(message)
75 | local fcc_str = message:get_header('FCC')
76 | message:remove_header('FCC')
77 | end
78 |
79 | function M.preview() end
80 |
81 | ---
82 |
83 | function M.delete_draft() end
84 |
85 | return M
86 |
--------------------------------------------------------------------------------
/lua/galore/hooks/post.lua:
--------------------------------------------------------------------------------
1 | --- After a mail have been sent
2 | local go = require('galore.gmime.object')
3 | -- Should config really be in here?
4 | local config = require('galore.config')
5 | local job = require('galore.jobs')
6 | local ffi = require('ffi')
7 |
8 | local M = {}
9 |
10 | -- XXX do error handling.
11 | function M.fcc_nm_insert(message)
12 | local obj = ffi.cast('GMimeObject *', message)
13 | local fcc_str = go.object_get_header(obj, 'Fcc')
14 |
15 | -- Should we try to guard users?
16 | -- this shouldn't be an abs path
17 | local path = config.values.fccdir .. '/' .. fcc_str
18 |
19 | go.object_set_header(obj, 'Fcc', nil)
20 | job.insert_mail(message, path, '')
21 | end
22 |
23 | --- write file to a maildir
24 | --- this should do a lot more checks
25 | --- create dirs etc if it doesn't exist
26 | function M.fcc_fs(message)
27 | local obj = ffi.cast('GMimeObject *', message)
28 | local fcc_str = go.object_get_header(obj, 'Fcc')
29 |
30 | go.object_set_header(obj, 'Fcc', nil)
31 | end
32 |
33 | function M.delete_draft(message) end
34 |
35 | return M
36 |
--------------------------------------------------------------------------------
/lua/galore/hooks/send.lua:
--------------------------------------------------------------------------------
1 | local gp = require('galore.gmime.parts')
2 | local M = {}
3 |
4 | --- hook design:
5 | --- They should log when they run
6 | --- A hook should be able to block and not.
7 |
8 | function M.preview_ask(message)
9 | -- create preview
10 | vim.ui.input({
11 | prompt = 'Do you want to send email? [Y]es/[N]o',
12 | }, function(input)
13 | if input then
14 | input = input:lower()
15 | if input == 'yes' or input == 'y' then
16 | return true
17 | end
18 | end
19 | return false
20 | end)
21 | end
22 |
23 | --- @param message any
24 | --- @return boolean
25 | function M.has_attachment(message)
26 | local state = {}
27 | local find_attachment = function(_, part)
28 | if gp.is_part(part) and gp.part_is_attachment(part) then
29 | state.attachment = true
30 | end
31 | end
32 | gp.message_foreach(message, find_attachment)
33 | return state.attachment
34 | end
35 |
36 | --- Doesn't handle re:
37 | function M.missed_attachment(message)
38 | local sub = gp.message_get_subject(message)
39 | if sub:match('[a,A]ttachment') and not M.has_attachment(message) then
40 | return false
41 | end
42 | return true
43 | end
44 |
45 | -- Delay sending for X seconds
46 | -- Returns a cancel channel and a delay-hook
47 | -- XXX api for async hooks?
48 | -- You don't use this directly but to make hooks
49 | function M.make_delay_hook(seconds) end
50 |
51 | return M
52 |
--------------------------------------------------------------------------------
/lua/galore/init.lua:
--------------------------------------------------------------------------------
1 | local config = require('galore.config')
2 |
3 | local galore_build_root = (function()
4 | local dirname = string.sub(debug.getinfo(1).source, 2, #'/init.lua' * -1)
5 | return dirname .. '../../build/src/'
6 | end)()
7 |
8 | local galore = {}
9 |
10 | galore.connected = false
11 |
12 | function galore.open(opts)
13 | opts = opts or {}
14 | galore.withconnect(function()
15 | config.values.init(opts)
16 | end)
17 | end
18 |
19 | local function env(var)
20 | local value = vim.fn.getenv(var)
21 | if value ~= vim.NIL then
22 | value = galore_build_root .. ':' .. value
23 | else
24 | value = galore_build_root
25 | end
26 | vim.fn.setenv(var, value)
27 | end
28 |
29 | function galore.connect()
30 | if not galore.connected then
31 | env('GI_TYPELIB_PATH')
32 | local lgi = require('lgi')
33 | local gmime = lgi.require('GMime', '3.0')
34 | gmime.init()
35 | local galorelib = lgi.require('Galore', '0.1')
36 | galorelib.init()
37 | local runtime = require('galore.runtime')
38 | if galore.user_config ~= nil then
39 | config.values = vim.tbl_deep_extend('force', config.values, galore.user_config)
40 | end
41 | runtime.init()
42 | end
43 | galore.connected = true
44 | end
45 |
46 | function galore.setup(opts)
47 | galore.user_config = opts
48 | end
49 |
50 | function galore.withconnect(func)
51 | if not galore.connected then
52 | galore.connect()
53 | end
54 | func()
55 | end
56 |
57 | function galore.compose(kind)
58 | galore.withconnect(function()
59 | local cb = require('galore.callback')
60 | cb.new_message(kind)
61 | end)
62 | end
63 |
64 | function galore.mailto(kind, str)
65 | local url = galore('galore.url')
66 | str = str:gsub('<%s*(.*)%s*>', '%1')
67 | local opts = url.parse_url(str)
68 |
69 | -- remove things we don't trust in a mailto
70 | opts = url.normalize(opts)
71 | galore.withconnect(function()
72 | local cb = require('galore.callback')
73 | cb.new_message(kind, opts)
74 | end)
75 | end
76 |
77 | function galore.xdg_install()
78 | local dirname = string.sub(debug.getinfo(1).source, 2, #'/init.lua' * -1)
79 | local str = dirname .. '../../' .. 'xdg_install.sh'
80 | vim.fn.system(str)
81 | end
82 |
83 | return galore
84 |
--------------------------------------------------------------------------------
/lua/galore/jobs.lua:
--------------------------------------------------------------------------------
1 | local config = require('galore.config')
2 | local runtime = require('galore.runtime')
3 | local Job = require('plenary.job')
4 | local log = require('galore.log')
5 |
6 | local lgi = require('lgi')
7 | local gmime = lgi.require('GMime', '3.0')
8 |
9 | local uv = vim.loop
10 |
11 | local M = {}
12 |
13 | function M.new()
14 | Job:new({
15 | command = 'notmuch',
16 | args = { 'new' },
17 | on_exit = function(_, ret_val)
18 | if ret_val == 0 then
19 | vim.notify('Notmuch updated successfully')
20 | else
21 | vim.notify('Notmuch update failed', vim.log.levels.ERROR)
22 | end
23 | end,
24 | }):start()
25 | end
26 |
27 | function M.get_type(file)
28 | local ret
29 | Job:new({
30 | command = 'file',
31 | args = { '-b', '--mime-type', file },
32 | on_exit = function(j, _)
33 | ret = j:result()
34 | end,
35 | }):sync()
36 | return ret
37 | end
38 |
39 | function M.html(text)
40 | local ret
41 | Job:new({
42 | command = 'html2text',
43 | args = {},
44 | writer = text,
45 | on_exit = function(j, _)
46 | ret = j:result()
47 | end,
48 | }):sync()
49 | return ret
50 | end
51 |
52 | -- add cols?
53 | function M.w3m(text)
54 | local ret
55 | Job:new({
56 | command = 'w3m',
57 | args = { '-dump', '-T', 'text/html' },
58 | writer = text,
59 | on_exit = function(j, code, signal)
60 | ret = j:result()
61 | end,
62 | }):sync()
63 | return ret
64 | end
65 |
66 | function M.pipe_str(cmd, text)
67 | local args = { unpack(cmd, 2) }
68 | Job:new({
69 | command = cmd[1],
70 | args = args,
71 | writer = text,
72 | on_exit = function(j, code, signal) end,
73 | }):sync()
74 | end
75 |
76 | --- Add a callback to this?
77 | --- TODO set env for testing etc
78 | local function raw_pipe(object, cmd, args, cb)
79 | local stdout = uv.new_pipe()
80 | local stderr = uv.new_pipe()
81 | local stdin = uv.new_pipe()
82 |
83 | local fds = uv.pipe({ nonblock = true }, { nonblock = true })
84 | -- local stream = gs.stream_pipe_new(fds.write)
85 | local stream = gmime.StreamPipe.new(fds.write)
86 | stdin:open(fds.read)
87 | local handle
88 | local pid
89 |
90 | local opts = {
91 | args = args,
92 | -- stdio = { fds.read, stdout, stderr}
93 | stdio = { stdin, stdout, stderr },
94 | }
95 |
96 | handle, pid = uv.spawn(cmd, opts, function(code, signal)
97 | stdin:close()
98 | stdout:close()
99 | stderr:close()
100 | vim.schedule(function()
101 | if code ~= 0 then
102 | log.log(cmd .. ' exited with: ' .. tostring(code), vim.log.levels.ERROR)
103 | else
104 | if cb then
105 | cb()
106 | end
107 | end
108 | -- add a cb here?
109 | end)
110 | end)
111 |
112 | --- Maybe something like this
113 | --- If you wanna pipe the buffer, don't use this
114 | --- this isn't async, but maybe it shouldn't be beacuse
115 | --- the function that calls raw_pipe should be async.
116 | --- since creating a message could be a pita too
117 | ---
118 | --- TODO
119 | --- should I just vim.schedule() this?
120 | --- Nope! You should install a worker!
121 | -- if gp.is_part(object) then
122 | if gmime.Part:is_type_of(object) then
123 | local part = object
124 | if part:is_attachment() then
125 | local dw = part:get_content()
126 | dw:write_to_stream(stream)
127 | else
128 | local r = require('galore.render')
129 | r.part_to_stream(part, {}, stream)
130 | end
131 | else
132 | object:write_to_stream(runtime.format_opts, stream)
133 | end
134 |
135 | stream:flush()
136 | stream:close()
137 |
138 | uv.read_start(stdout, function(err, data)
139 | assert(not err, err)
140 | -- we shouldn't really do anything with the data, maybe log it
141 | if data then
142 | print(data)
143 | end
144 | end)
145 |
146 | uv.read_start(stderr, function(err, data)
147 | assert(not err, err)
148 | if data then
149 | print('stderr: ', data)
150 | end
151 | end)
152 |
153 | uv.shutdown(stdin, function()
154 | print('stdin shutdown', stdin)
155 | uv.close(handle, function()
156 | print('process closed', handle, pid)
157 | end)
158 | end)
159 | end
160 |
161 | function M.insert_mail(message, folder, tags)
162 | local parent_dir = config.values.select_dir(message)
163 | local folderflag = string.format('--folder=%s%s', parent_dir, folder)
164 | local args = vim.tbl_flatten({ 'insert', '--create-folder', folderflag, tags })
165 | raw_pipe(message, 'notmuch', args)
166 | end
167 |
168 | --- being able to spawn in a terminal
169 | function M.send_mail(message, cb)
170 | local cmd, args = config.values.send_cmd(message)
171 | raw_pipe(message, cmd, args, cb)
172 | end
173 |
174 | function M.pipe_input(object)
175 | vim.ui.input({
176 | prompt = 'command: ',
177 | }, function(ret)
178 | if ret ~= nil then
179 | local cmd = vim.split(ret, ' ')
180 | raw_pipe(cmd, object)
181 | end
182 | end)
183 | end
184 |
185 | --- @param cmd string[]
186 | --- @param obj gmime.MimeObject
187 | function M.pipe(cmd, obj)
188 | local args = { unpack(cmd, 2) }
189 | raw_pipe(obj, cmd[1], args)
190 | end
191 |
192 | return M
193 |
--------------------------------------------------------------------------------
/lua/galore/lib/buffer.lua:
--------------------------------------------------------------------------------
1 | local Buffer = {}
2 |
3 | function Buffer:new(this)
4 | this = this or {}
5 | self.__index = self
6 | setmetatable(this, self)
7 |
8 | return this
9 | end
10 |
11 | function Buffer:focus()
12 | local windows = vim.fn.win_findbuf(self.handle)
13 |
14 | if #windows == 0 then
15 | vim.api.nvim_win_set_buf(0, self.handle)
16 | return
17 | end
18 |
19 | vim.fn.win_gotoid(windows[1])
20 | end
21 |
22 | function Buffer:lock()
23 | self:set_option('readonly', true)
24 | self:set_option('modifiable', false)
25 | end
26 |
27 | function Buffer:unlock()
28 | self:set_option('readonly', false)
29 | self:set_option('modifiable', true)
30 | end
31 |
32 | function Buffer:is_open()
33 | return #vim.fn.win_findbuf(self.handle) ~= 0
34 | end
35 |
36 | function Buffer:clear()
37 | vim.api.nvim_buf_set_lines(self.handle, 0, -1, false, {})
38 | end
39 |
40 | function Buffer:set_parent(parent)
41 | self.parent = parent
42 | end
43 |
44 | function Buffer:add_timer(timer)
45 | if self.timers == nil then
46 | self.timers = {}
47 | end
48 | table.insert(self.timers, timer)
49 | end
50 |
51 | function Buffer:stop_timers()
52 | for _, timer in ipairs(self.timers) do
53 | timer:stop()
54 | end
55 | -- free timers so we don't have backpointers etc
56 | self.timers = {}
57 | end
58 |
59 | local function cleanup(self)
60 | if self.cleanup then
61 | self:cleanup()
62 | end
63 | self.timers = {}
64 | self.parent = nil
65 | end
66 |
67 | -- split up this and add a callback handler for when the buffer is deleted
68 | function Buffer:close(delete)
69 | local bufwins = #vim.fn.win_findbuf(self.handle)
70 |
71 | local floating = vim.api.nvim_win_get_config(0).zindex
72 |
73 | if floating then
74 | vim.api.nvim_win_close(0, true)
75 | if self.parent then
76 | self.parent:focus()
77 | end
78 | elseif self.kind == 'replace' and self.parent then
79 | if self.parent.handle and vim.fn.bufexists(self.parent.handle) ~= 0 then
80 | vim.api.nvim_win_set_buf(0, self.parent.handle)
81 | end
82 | -- elseif self.kind == "floating" then
83 | -- vim.api.nvim_win_close(0, true)
84 | -- if self.parent then
85 | -- self.parent:focus()
86 | -- end
87 | else
88 | if self.parent then
89 | self.parent:focus()
90 | end
91 | end
92 | if delete and bufwins <= 1 then
93 | if self.runner then
94 | self.runner.close()
95 | end
96 | if self.timers then
97 | self:stop_timers()
98 | end
99 | cleanup(self)
100 | vim.api.nvim_buf_delete(self.handle, {})
101 | end
102 | end
103 |
104 | function Buffer:get_lines(first, last, strict)
105 | return vim.api.nvim_buf_get_lines(self.handle, first, last, strict)
106 | end
107 |
108 | function Buffer:get_line(line)
109 | return vim.fn.getbufline(self.handle, line)
110 | end
111 |
112 | function Buffer:get_current_line()
113 | return self:get_line(vim.fn.getpos('.')[2])
114 | end
115 |
116 | function Buffer:set_lines(first, last, strict, lines)
117 | vim.api.nvim_buf_set_lines(self.handle, first, last, strict, lines)
118 | end
119 |
120 | function Buffer:set_text(first_line, last_line, first_col, last_col, lines)
121 | vim.api.nvim_buf_set_text(self.handle, first_line, first_col, last_line, last_col, lines)
122 | end
123 |
124 | function Buffer:move_cursor(line)
125 | if line < 0 then
126 | self:focus()
127 | vim.cmd('norm G')
128 | else
129 | self:focus()
130 | vim.cmd('norm ' .. line .. 'G')
131 | end
132 | end
133 |
134 | function Buffer:is_valid()
135 | return vim.api.nvim_buf_is_valid(self.handle)
136 | end
137 |
138 | function Buffer:put(lines, after, follow)
139 | self:focus()
140 | vim.api.nvim_put(lines, 'l', after, follow)
141 | end
142 |
143 | function Buffer:create_fold(first, last)
144 | vim.cmd(string.format('%d,%dfold', first, last))
145 | vim.cmd(string.format('%d,%dfoldopen', first, last))
146 | end
147 |
148 | function Buffer:get_option(name)
149 | vim.api.nvim_buf_get_option(self.handle, name)
150 | end
151 |
152 | function Buffer:set_option(name, value)
153 | vim.api.nvim_buf_set_option(self.handle, name, value)
154 | end
155 |
156 | function Buffer:set_name(name)
157 | vim.api.nvim_buf_set_name(self.handle, name)
158 | end
159 |
160 | function Buffer:set_foldlevel(level)
161 | local windows = vim.fn.win_findbuf(self.handle)
162 | vim.api.nvim_win_set_option(0, 'foldlevel', level)
163 | end
164 |
165 | function Buffer:replace_content_with(lines)
166 | self:set_lines(0, -1, false, lines)
167 | end
168 |
169 | function Buffer:open_fold(line, reset_pos)
170 | local pos
171 | if reset_pos == true then
172 | pos = vim.fn.getpos()
173 | end
174 |
175 | vim.fn.setpos('.', { self.handle, line, 0, 0 })
176 | vim.cmd('normal zo')
177 |
178 | if reset_pos == true then
179 | vim.fn.setpos('.', pos)
180 | end
181 | end
182 |
183 | -- remove?
184 | function Buffer:set_ns(name)
185 | self.ns = vim.api.nvim_create_namespace(name)
186 | end
187 |
188 | function Buffer:add_highlight(line, col_start, col_end, name)
189 | local ns_id = self.ns or 0
190 | vim.api.nvim_buf_add_highlight(self.handle, ns_id, name, line, col_start, col_end)
191 | end
192 |
193 | function Buffer:unplace_sign(id)
194 | vim.cmd('sign unplace ' .. id)
195 | end
196 |
197 | function Buffer:place_sign(line, name, group, id)
198 | -- Sign IDs should be unique within a group, however there's no downside as
199 | -- long as we don't want to uniquely identify the placed sign later. Thus,
200 | -- we leave the choice to the caller
201 | local sign_id = id or 1
202 |
203 | -- There's an equivalent function sign_place() which can automatically use
204 | -- a free ID, but is considerable slower, so we use the command for now
205 | local cmd = 'sign place ' .. sign_id .. ' line=' .. line .. ' name=' .. name
206 | if group ~= nil then
207 | cmd = cmd .. ' group=' .. group
208 | end
209 | cmd = cmd .. ' buffer=' .. self.handle
210 |
211 | vim.cmd(cmd)
212 | return sign_id
213 | end
214 |
215 | function Buffer:get_sign_at_line(line, group)
216 | group = group or '*'
217 | return vim.fn.sign_getplaced(self.handle, {
218 | group = group,
219 | lnum = line,
220 | })[1]
221 | end
222 |
223 | function Buffer:clear_sign_group(group)
224 | vim.cmd('sign unplace * group=' .. group .. ' buffer=' .. self.handle)
225 | end
226 |
227 | function Buffer:set_filetype(ft)
228 | self:set_option('filetype', ft)
229 | end
230 |
231 | function Buffer:call(f)
232 | vim.api.nvim_buf_call(self.handle, f)
233 | end
234 |
235 | function Buffer.exists(name)
236 | return vim.fn.bufnr(name) ~= -1
237 | end
238 |
239 | function Buffer:set_extmark(...)
240 | return vim.api.nvim_buf_set_extmark(self.handle, ...)
241 | end
242 |
243 | function Buffer:get_extmark(ns, id)
244 | return vim.api.nvim_buf_get_extmark_by_id(self.handle, ns, id, { details = true })
245 | end
246 |
247 | function Buffer:del_extmark(ns, id)
248 | return vim.api.nvim_buf_del_extmark(self.handle, ns, id)
249 | end
250 |
251 | local function make_new(name)
252 | if vim.fn.bufexists(name) ~= 0 then
253 | local buf = vim.fn.bufnr(name)
254 | vim.api.nvim_win_set_buf(0, buf)
255 | return true
256 | end
257 | end
258 |
259 | function Buffer.create(config, class)
260 | config = config or {}
261 |
262 | local kind = config.kind or 'replace'
263 | local buffer = nil
264 | class = class or Buffer
265 |
266 | if kind == 'replace' then
267 | if make_new(config.name) then
268 | return
269 | end
270 | vim.cmd('enew')
271 | buffer = class:new({ handle = vim.api.nvim_get_current_buf() })
272 | elseif kind == 'tab' then
273 | vim.cmd('tabnew')
274 | if make_new(config.name) then
275 | return
276 | end
277 | buffer = class:new({ handle = vim.api.nvim_get_current_buf() })
278 | elseif kind == 'split' then
279 | vim.cmd('below new')
280 | if make_new(config.name) then
281 | return
282 | end
283 | buffer = class:new({ handle = vim.api.nvim_get_current_buf() })
284 | elseif kind == 'split_above' then
285 | vim.cmd('top new')
286 | if make_new(config.name) then
287 | return
288 | end
289 | buffer = class:new({ handle = vim.api.nvim_get_current_buf() })
290 | elseif kind == 'vsplit' then
291 | vim.cmd('bot vnew')
292 | if make_new(config.name) then
293 | return
294 | end
295 | buffer = class:new({ handle = vim.api.nvim_get_current_buf() })
296 | elseif kind == 'floating' then
297 | -- Creates the border window
298 | -- TODO maybe do something fancy, like checking if we
299 | -- there are more floating windows shift the windows
300 | local vim_height = vim.api.nvim_eval([[&lines]])
301 | local vim_width = vim.api.nvim_eval([[&columns]])
302 | local width = math.floor(vim_width * 0.8) + 5
303 | local height = math.floor(vim_height * 0.7) + 2
304 | local col = vim_width * 0.1 - 2
305 | local row = vim_height * 0.15 - 1
306 |
307 | local content_buffer
308 | local inited = false
309 | if vim.fn.bufexists(config.name) ~= 0 then
310 | content_buffer = vim.fn.bufnr(config.name)
311 | inited = true
312 | else
313 | content_buffer = vim.api.nvim_create_buf(true, true)
314 | end
315 |
316 | local content_window = vim.api.nvim_open_win(content_buffer, true, {
317 | relative = 'editor',
318 | width = width,
319 | height = height,
320 | col = col,
321 | row = row,
322 | style = 'minimal',
323 | focusable = false,
324 | border = 'single',
325 | })
326 |
327 | buffer = class:new({ handle = content_buffer })
328 | if inited then
329 | return
330 | end
331 | vim.wo.winhl = 'Normal:Normal'
332 | vim.api.nvim_win_set_cursor(content_window, { 1, 0 })
333 | else
334 | assert('Wrong buffer mode!')
335 | end
336 |
337 | buffer.kind = kind
338 | buffer.parent = config.parent
339 | buffer.cleanup = config.cleanup
340 | buffer.update = config.update
341 |
342 | vim.wo.nu = false
343 | vim.wo.rnu = false
344 |
345 | if config.name then
346 | buffer:set_name(config.name)
347 | end
348 |
349 | buffer:set_option('buflisted', config.buflisted or false)
350 | buffer:set_option('bufhidden', config.bufhidden or 'hide')
351 | buffer:set_option('buftype', config.buftype or 'nofile')
352 | buffer:set_option('swapfile', false)
353 | buffer:set_option('fileencoding', 'utf-8')
354 | buffer:set_option('fileformat', 'unix')
355 |
356 | -- don't want to do it like this
357 | vim.api.nvim_win_set_option(0, 'wrap', false)
358 |
359 | if config.ft then
360 | buffer:set_filetype(config.ft)
361 | end
362 |
363 | local mapopts = { noremap = true, silent = true, buffer = buffer.handle }
364 | if config.mappings then
365 | for mode, val in pairs(config.mappings) do
366 | for key, cb in pairs(val) do
367 | local opts = mapopts
368 | if type(cb) == 'table' then
369 | opts = vim.tbl_extend('keep', cb, mapopts)
370 | opts.rhs = nil
371 | cb = cb.rhs
372 | end
373 | local cbfunc = function()
374 | cb(buffer)
375 | end
376 | vim.keymap.set(mode, key, cbfunc, opts)
377 | end
378 | end
379 | else
380 | local conf = require('galore.config')
381 | for mode, val in pairs(conf.values.key_bindings.default) do
382 | for key, cb in pairs(val) do
383 | local cbfunc = function()
384 | cb(buffer)
385 | end
386 | vim.keymap.set(mode, key, cbfunc, mapopts)
387 | end
388 | end
389 | end
390 |
391 | if config.init then
392 | config.init(buffer)
393 | end
394 |
395 | if config.cursor == 'top' then
396 | vim.api.nvim_win_set_cursor(0, { 1, 0 })
397 | end
398 |
399 | -- if config.autocmds then
400 | -- -- vim.api.nvim_create_augroup("") -- unique id?
401 | -- for event, cb in pairs(config.autocmds) do
402 | -- local cbfunc = function ()
403 | -- cb(buffer)
404 | -- end
405 | -- vim.api.nvim_create_autocmd(event, {callback = cbfunc, buffer = buffer.handle})
406 | -- end
407 | -- end
408 | vim.api.nvim_create_autocmd('BufDelete', {
409 | callback = function()
410 | -- buffer_delete(buffer.handle)
411 | end,
412 | buffer = buffer.handle,
413 | })
414 |
415 | -- if not config.modifiable then
416 | -- buffer:set_option("modifiable", false)
417 | -- end
418 |
419 | if config.readonly ~= nil and config.readonly then
420 | buffer:set_option('readonly', true)
421 | end
422 | vim.b.galorebuf = function()
423 | return buffer
424 | end
425 |
426 | return buffer
427 | end
428 |
429 | return Buffer
430 |
--------------------------------------------------------------------------------
/lua/galore/lib/ordered.lua:
--------------------------------------------------------------------------------
1 | local Ordered = {}
2 |
3 | function Ordered.insert(t, k, v)
4 | if v == nil then
5 | t.remove(k)
6 | return
7 | end
8 | if not rawget(t._values, k) then
9 | table.insert(t._list, k)
10 | t._values[k] = v
11 | end
12 | end
13 |
14 | local function find(t, v)
15 | for i, v2 in ipairs(t) do
16 | if v == v2 then
17 | return i
18 | end
19 | end
20 | end
21 |
22 | function Ordered.remove(t, k)
23 | local v = t._values[k]
24 | if v then
25 | table.remove(t._list, find(t._list, k))
26 | t._values[k] = nil
27 | end
28 | end
29 |
30 | function Ordered.index(t, k)
31 | return rawget(t._values, k)
32 | end
33 |
34 | function Ordered.pairs(t)
35 | local i = 0
36 | return function()
37 | i = i + 1
38 | local key = t._list[i]
39 | if key ~= nil then
40 | return key, t._values[key]
41 | end
42 | end
43 | end
44 |
45 | function Ordered.new()
46 | local tbl = { _values = {}, _list = {} }
47 | return setmetatable(tbl, {
48 | __newindex = Ordered.insert,
49 | __len = function(t)
50 | return #t._list
51 | end,
52 | __pairs = Ordered.pairs, -- doesn't work in luajit
53 | __index = tbl._values,
54 | })
55 | end
56 |
57 | return Ordered
58 |
--------------------------------------------------------------------------------
/lua/galore/lib/timers.lua:
--------------------------------------------------------------------------------
1 | local uv = vim.loop
2 | local ok, notify = pcall(require, 'notify')
3 |
4 | local M = {}
5 |
6 | if not ok then
7 | --- log this
8 | vim.api.nvim_err_writeln("Can't find nvim-notify, will try to fallback")
9 | notify = vim.notify
10 | end
11 |
12 | local function default_opts(opts)
13 | opts.title = opts.title or 'Timer running'
14 | opts.stop_title = opts.stop_title or 'Timer stopped'
15 | opts.done_msg = opts.done_msg or 'Timer hit 0!'
16 | opts.tick = 1000
17 | opts.time_to_level = opts.time_to_level or function()
18 | return vim.log.levels.INFO
19 | end
20 | end
21 |
22 | local function time_to_str(time)
23 | local seconds = math.floor(time / 1000)
24 | local min = math.floor(seconds / 60)
25 | local hours = math.floor(min / 60)
26 | min = min - hours * 60
27 | seconds = seconds - min * 60
28 | if hours ~= 0 then
29 | return string.format('%d:%d:%d', hours, min, seconds)
30 | end
31 | if min ~= 0 then
32 | return string.format('%d:%d', min, seconds)
33 | end
34 | return string.format('%d', seconds)
35 | end
36 |
37 | --- Creates a popup_timer
38 | --- @param time integer how long we should run the timer
39 | --- @param rep integer not used atm
40 | --- @param callback function()
41 | --- @param opts table timer costumization
42 | function M.popup_timer(time, rep, callback, opts)
43 | opts = opts or {}
44 | default_opts(opts)
45 | local id = notify(time_to_str(time), vim.log.levels.INFO, {
46 | title = opts.title,
47 | render = opts.render,
48 | timeout = opts.tick + 1000,
49 | })
50 | local timer = uv.new_timer()
51 | timer:start(
52 | opts.tick,
53 | opts.tick,
54 | vim.schedule_wrap(function()
55 | time = time - opts.tick
56 | if time <= 0 then
57 | timer:stop()
58 | notify(opts.done_msg, vim.log.levels.INFO, {
59 | title = opts.stop_title,
60 | replace = id,
61 | timeout = opts.timeout,
62 | })
63 | callback()
64 | -- if rep > 0 then
65 | -- M.popup_timer(rep, rep, callback, opts)
66 | -- end
67 | return
68 | end
69 | id = notify(time_to_str(time), opts.time_to_level(time), {
70 | title = opts.title,
71 | replace = id,
72 | timeout = opts.tick + 1000,
73 | })
74 | end)
75 | )
76 | return timer
77 | end
78 |
79 | return M
80 |
--------------------------------------------------------------------------------
/lua/galore/log.lua:
--------------------------------------------------------------------------------
1 | local function show_level(level)
2 | if level == vim.log.levels.TRACE then
3 | return 'TRACE'
4 | elseif level == vim.log.levels.DEBUG then
5 | return 'DEBUG'
6 | elseif level == vim.log.levels.INFO then
7 | return 'INFO'
8 | elseif level == vim.log.levels.WARN then
9 | return 'WARN'
10 | elseif level == vim.log.levels.ERROR then
11 | return 'ERROR'
12 | -- elseif level == vim.log.levels.OFF then
13 | end
14 | end
15 |
16 | local p_debug = vim.fn.getenv('DEBUG_GALORE')
17 | if p_debug == vim.NIL then
18 | p_debug = false
19 | end
20 |
21 | -- User configuration section
22 | local default_config = {
23 | -- Name of the plugin. Prepended to log messages
24 | plugin = 'Galore',
25 |
26 | notify = true,
27 |
28 | -- Should write to a file
29 | use_file = true,
30 | -- default_level
31 |
32 | -- Should write to the quickfix list
33 | -- use notify to quickfix instead
34 | -- use_quickfix = false,
35 |
36 | -- Any messages above this level will be logged.
37 | level = p_debug and vim.log.levels.DEBUG or vim.log.levels.INFO,
38 | }
39 |
40 | local log = {}
41 |
42 | log.new = function(config)
43 | config = vim.tbl_deep_extend('force', default_config, config)
44 |
45 | local outfile =
46 | string.format('%s/%s.log', vim.api.nvim_call_function('stdpath', { 'cache' }), config.plugin)
47 |
48 | local obj = {}
49 | local logfun = function(str, level)
50 | if not level then
51 | level = vim.log.levels.INFO
52 | end
53 | if level < config.level then
54 | return
55 | end
56 |
57 | local info = debug.getinfo(2, 'Sl')
58 | local lineinfo = info.short_src .. ':' .. info.currentline
59 |
60 | -- Send a notify if we have that configured
61 | if config.notify then
62 | local message = string.format('[%s] %s', config.plugin, str)
63 | vim.notify(message, level)
64 | end
65 |
66 | local levelstr = show_level(level)
67 | -- Output to log file
68 | if config.use_file then
69 | local fp = assert(io.open(outfile, 'a'))
70 | local message = string.format('[%-6s%s] %s: %s\n', levelstr, os.date(), lineinfo, str)
71 | fp:write(message)
72 | fp:close()
73 | end
74 |
75 | -- maybe
76 | -- Output to quickfix
77 | if config.use_quickfix then
78 | local formatted_msg = string.format('[%s] %s', levelstr, str)
79 | local qf_entry = {
80 | -- remove the @ getinfo adds to the file path
81 | filename = info.source:sub(2),
82 | lnum = info.currentline,
83 | col = 1,
84 | text = formatted_msg,
85 | }
86 | vim.fn.setqflist({ qf_entry }, 'a')
87 | end
88 | end
89 | local log_err = function(str)
90 | logfun(str, vim.log.levels.ERROR)
91 | error(str)
92 | end
93 | obj.log = logfun
94 | obj.log_err = log_err
95 | return obj
96 | end
97 |
98 | -- return log
99 | -- pass config-value to log.new()?
100 | return log.new({})
101 |
--------------------------------------------------------------------------------
/lua/galore/message_browser.lua:
--------------------------------------------------------------------------------
1 | local Buffer = require('galore.lib.buffer')
2 | local o = require('galore.opts')
3 | local async = require('plenary.async')
4 | local browser = require('galore.browser')
5 |
6 | local Mb = Buffer:new()
7 |
8 | local function mb_get(self)
9 | local first = true
10 | self.highlight = {}
11 | return browser.get_entries(self, 'show-message', function(message, n)
12 | if message then
13 | table.insert(self.State, message.id)
14 | if first then
15 | vim.api.nvim_buf_set_lines(self.handle, 0, -1, false, { message.entry })
16 | first = false
17 | else
18 | vim.api.nvim_buf_set_lines(self.handle, -1, -1, false, { message.entry })
19 | end
20 | if message.highlight then
21 | table.insert(self.highlight, n)
22 | vim.api.nvim_buf_add_highlight(self.handle, self.dians, 'GaloreHighlight', n, 0, -1)
23 | end
24 | return 1
25 | end
26 | end)
27 | end
28 |
29 | function Mb:async_runner()
30 | self.updating = true
31 | local func = async.void(function()
32 | self.runner = mb_get(self)
33 | pcall(function()
34 | self.runner.resume()
35 | self:lock()
36 | self.updating = false
37 | end)
38 | end)
39 | func()
40 | end
41 |
42 | function Mb:refresh()
43 | if self.runner then
44 | self.runner.close()
45 | self.runner = nil
46 | end
47 | self:unlock()
48 | self:clear()
49 | self:async_runner()
50 | end
51 |
52 | function Mb:update(line_nr)
53 | local id = self.State[line_nr]
54 | browser.update_lines_helper(self, 'show-message', 'id:' .. id, line_nr)
55 | end
56 |
57 | function Mb:commands()
58 | vim.api.nvim_buf_create_user_command(self.handle, 'GaloreChangetag', function(args)
59 | if args.args then
60 | local callback = require('galore.callback')
61 | callback.change_tag(self, args)
62 | end
63 | end, {
64 | nargs = '*',
65 | })
66 | end
67 |
68 | -- create a browser class
69 | function Mb:create(search, opts)
70 | o.mb_options(opts)
71 | Buffer.create({
72 | name = opts.bufname(search),
73 | ft = 'galore-browser',
74 | kind = opts.kind,
75 | cursor = 'top',
76 | parent = opts.parent,
77 | mappings = opts.key_bindings,
78 | init = function(buffer)
79 | buffer.opts = opts
80 | buffer.search = search
81 | buffer.dians = vim.api.nvim_create_namespace('galore-dia')
82 | buffer:refresh()
83 | buffer:commands()
84 | if opts.limit then
85 | browser.scroll(buffer)
86 | end
87 | opts.init(buffer)
88 | end,
89 | }, Mb)
90 | end
91 |
92 | return Mb
93 |
--------------------------------------------------------------------------------
/lua/galore/message_view.lua:
--------------------------------------------------------------------------------
1 | local r = require('galore.render')
2 | local u = require('galore.util')
3 | local Buffer = require('galore.lib.buffer')
4 | local views = require('galore.views')
5 | local ui = require('galore.ui')
6 | local nu = require('galore.notmuch-util')
7 | local nm = require('notmuch')
8 | local runtime = require('galore.runtime')
9 | local browser = require('galore.browser')
10 | local o = require('galore.opts')
11 | local gu = require('galore.gmime-util')
12 |
13 | local Message = Buffer:new()
14 |
15 | --- add opts
16 | function Message:select_attachment(cb)
17 | local files = {}
18 | for _, v in ipairs(self.state.attachments) do
19 | table.insert(files, v.filename)
20 | end
21 | vim.ui.select(files, {
22 | prompt = 'Select attachment: ',
23 | }, function(item, idx)
24 | if item then
25 | cb(self.state.attachments[idx])
26 | else
27 | error('No file selected')
28 | end
29 | end)
30 | end
31 |
32 | function Message:update()
33 | self:unlock()
34 | self:clear()
35 |
36 | local message = gu.parse_message(self.line.filenames[self.index])
37 | -- au.process_au(message, line)
38 | if message then
39 | vim.api.nvim_buf_clear_namespace(self.handle, self.ns, 0, -1)
40 | self.message = message
41 | local buffer = {}
42 |
43 | r.show_headers(message, self.handle, { ns = self.ns }, self.line)
44 | local offset = vim.fn.line('$') - 1
45 | self.state = r.render_message(r.default_render, message, buffer, {
46 | offset = offset,
47 | keys = self.line.keys,
48 | })
49 | u.purge_empty(buffer)
50 | self:set_lines(-1, -1, true, buffer)
51 | local ns_line = vim.fn.line('$') - 1
52 | if not vim.tbl_isempty(self.state.attachments) then
53 | ui.render_attachments(self.state.attachments, ns_line, self.handle, self.ns)
54 | end
55 | vim.schedule(function()
56 | for i, cb in ipairs(self.state.callbacks) do
57 | cb(self.handle, self.ns)
58 | self.state.callbacks[i] = nil
59 | end
60 | end)
61 | end
62 | self:lock()
63 | end
64 |
65 | function Message:redraw()
66 | self:focus()
67 | self:update()
68 | end
69 |
70 | local function mark_read(self, pb, line, vline)
71 | runtime.with_db_writer(function(db)
72 | self.opts.tag_unread(db, line.id)
73 | nu.tag_if_nil(db, line.id, self.opts.empty_tag)
74 | nu.update_line(db, line)
75 | end)
76 | if vline and pb then
77 | pb:update(vline)
78 | end
79 | end
80 |
81 | function Message:next()
82 | if self.vline and self.parent then
83 | local mid, vline = browser.next(self.parent, self.vline)
84 | Message:create(mid, { kind = 'replace', parent = self.parent, vline = vline })
85 | end
86 | end
87 | --
88 | function Message:prev()
89 | if self.vline and self.parent then
90 | local mid, vline = browser.prev(self.parent, self.vline)
91 | Message:create(mid, { kind = 'replace', parent = self.parent, vline = vline })
92 | end
93 | end
94 |
95 | function Message:version_next()
96 | self.index = math.max(#self.line.filenames, self.index + 1)
97 | self:redraw()
98 | end
99 |
100 | function Message:version_prev()
101 | self.index = math.min(1, self.index - 1)
102 | self:redraw()
103 | end
104 |
105 | function Message:commands()
106 | vim.api.nvim_buf_create_user_command(self.handle, 'GaloreSaveAttachment', function(args)
107 | if args.fargs then
108 | local save_path = '.'
109 | if #args.fargs > 2 then
110 | save_path = args.fargs[2]
111 | end
112 | views.save_attachment(self.state.attachments, args.fargs[1], save_path)
113 | end
114 | end, {
115 | nargs = '*',
116 | complete = function()
117 | local files = {}
118 | for _, v in ipairs(self.state.attachments) do
119 | table.insert(files, v.filename)
120 | end
121 | return files
122 | end,
123 | })
124 | end
125 |
126 | function Message:thread_view()
127 | local tw = require('galore.thread_view')
128 | local opts = o.bufcopy(self.opts)
129 | opts.index = self.line.index
130 | tw:create(self.line.tid, opts)
131 | end
132 |
133 | function Message:create(mid, opts)
134 | o.view_options(opts)
135 | local line
136 | runtime.with_db(function(db)
137 | local message = nm.db_find_message(db, mid)
138 | line = nu.get_message(message)
139 | nu.line_populate(db, line)
140 | end)
141 | Buffer.create({
142 | name = opts.bufname(line.filenames[1]),
143 | ft = 'mail',
144 | kind = opts.kind,
145 | parent = opts.parent,
146 | cursor = 'top',
147 | mappings = opts.key_bindings,
148 | init = function(buffer)
149 | buffer.line = line
150 | buffer.opts = opts
151 | buffer.index = opts.index or 1
152 | buffer.vline = opts.vline
153 | buffer.ns = vim.api.nvim_create_namespace('galore-message-view')
154 | buffer.dians = vim.api.nvim_create_namespace('galore-dia')
155 | mark_read(buffer, opts.parent, line, opts.vline)
156 | buffer:update()
157 | buffer:commands()
158 | opts.init(buffer)
159 | end,
160 | }, Message)
161 | end
162 |
163 | function Message.open_attach() end
164 |
165 | return Message
166 |
--------------------------------------------------------------------------------
/lua/galore/notmuch-util.lua:
--------------------------------------------------------------------------------
1 | -- local nm = require("galore.notmuch")
2 | local nm = require('notmuch')
3 | local u = require('galore.util')
4 |
5 | local M = {}
6 |
7 | --- The thread will be freed on return, don't return the thread
8 | --- @param message any a message
9 | --- @param f function a function that takes a thread
10 | --- @return any returns the value after running f
11 | function M.message_with_thread(message, f)
12 | local id = nm.message_get_thread_id(message)
13 | local db = nm.message_get_db(message)
14 | local query = nm.create_query(db, 'thread:' .. id)
15 | for thread in nm.query_get_threads(query) do
16 | return f(thread)
17 | end
18 | end
19 |
20 | --- Get a single message and convert it into a line
21 | function M.get_message(message)
22 | local id = nm.message_get_id(message)
23 | local tid = nm.message_get_thread_id(message)
24 | local filenames = u.collect(nm.message_get_filenames(message))
25 | local sub = nm.message_get_header(message, 'Subject')
26 | local tags = u.collect(nm.message_get_tags(message))
27 | local from = nm.message_get_header(message, 'From')
28 | local date = nm.message_get_date(message)
29 | local key = u.collect(nm.message_get_properties(message, 'session-key', true))
30 | return {
31 | id = id,
32 | tid = tid,
33 | filenames = filenames,
34 | level = 1,
35 | pre = '',
36 | index = 1,
37 | total = 1,
38 | date = date,
39 | from = from,
40 | sub = sub,
41 | tags = tags,
42 | key = key,
43 | }
44 | end
45 |
46 | local function pop_helper(line, iter, i)
47 | for message in iter do
48 | if line.id == nm.message_get_id(message) then
49 | line.index = i
50 | break
51 | end
52 | local new_iter = nm.message_get_replies(message)
53 | i = pop_helper(line, new_iter, i + 1)
54 | end
55 | return i
56 | end
57 |
58 | function M.line_populate(db, line)
59 | local id = line.id
60 | local query = nm.create_query(db, 'mid:' .. id)
61 | for thread in nm.query_get_threads(query) do
62 | local i = 1
63 | line.total = nm.thread_get_total_messages(thread)
64 | local iter = nm.thread_get_toplevel_messages(thread)
65 | pop_helper(line, iter, i)
66 | end
67 | end
68 |
69 | local function update_tags(message, changes)
70 | nm.message_freeze(message)
71 | for _, change in ipairs(changes) do
72 | local status
73 | local op = string.sub(change, 1, 1)
74 | local tag = string.sub(change, 2)
75 | if op == '-' then
76 | status = nm.message_remove_tag(message, tag)
77 | elseif op == '+' then
78 | status = nm.message_add_tag(message, tag)
79 | end
80 | if status ~= nil then
81 | -- print error
82 | end
83 | end
84 | nm.message_thaw(message)
85 | nm.message_tags_to_maildir_flags(message)
86 | end
87 |
88 | function M.change_tag(db, id, str)
89 | local changes = vim.split(str, ' ')
90 | local message = nm.db_find_message(db, id)
91 | if message == nil then
92 | vim.notify("Can't change tag, message not found", vim.log.levels.ERROR)
93 | return
94 | end
95 | nm.db_atomic_begin(db)
96 | update_tags(message, changes)
97 | nm.db_atomic_end(db)
98 | end
99 |
100 | function M.tag_if_nil(db, id, tag)
101 | local message = nm.db_find_message(db, id)
102 | local tags = u.collect(nm.message_get_tags(message))
103 | if vim.tbl_isempty(tags) and tag then
104 | M.change_tag(db, id, tag)
105 | end
106 | end
107 |
108 | function M.update_line(db, line_info)
109 | local message = nm.db_find_message(db, line_info.id)
110 | local new_info = M.get_message(message)
111 | line_info.id = new_info.id
112 | line_info.filenames = new_info.filenames
113 | line_info.tags = new_info.tags
114 | end
115 |
116 | return M
117 |
--------------------------------------------------------------------------------
/lua/galore/opts.lua:
--------------------------------------------------------------------------------
1 | -- Populate default options from the config file
2 | local config = require('galore.config')
3 | local builder = require('galore.builder')
4 |
5 | local M = {}
6 |
7 | --- TODO how should keybindings work, should we extend the table or overwrite it?
8 |
9 | local function parse_bufname(opt, def)
10 | if type(opt) == 'string' then
11 | return function()
12 | return opt
13 | end
14 | elseif type(opt) == 'function' then
15 | return opt
16 | elseif type(opt) == 'nil' then
17 | return def
18 | else
19 | error('Bad argument to bufname, only function(string) or string is allowed')
20 | end
21 | end
22 |
23 | local function fallbacks(...)
24 | for i = 1, select('#', ...) do
25 | local value = select(i, ...)
26 | if value ~= nil then
27 | return value
28 | end
29 | end
30 | end
31 |
32 | local function keybindings(opt, def)
33 | return fallbacks(opt, def, config.values.key_bindings.default)
34 | end
35 |
36 | --- values that should be copied when cloning opts from one buffer to another.
37 | --- atm we copy nothing and rely on the default opts, maybe this isn't what we
38 | --- want. We let future tell.
39 | function M.bufcopy(opts)
40 | -- local new = vim.deepcopy(opts)
41 | return {}
42 | end
43 |
44 | local function runtime(opts)
45 | opts.runtime = vim.F.if_nil(opts.runtime, {})
46 | opts.runtime.config = vim.F.if_nil(opts.runtime.config, config.values.nm_config)
47 | opts.runtime.db_path = vim.F.if_nil(opts.runtime.db_path, config.values.db_path)
48 | opts.runtime.profile = vim.F.if_nil(opts.runtime.profile, config.values.nm_profile)
49 | opts.runtime.mail_root = vim.F.if_nil(opts.runtime.config, config.values.mail_root)
50 | opts.runtime.exclude_tags =
51 | vim.F.if_nil(opts.runtime.exclude_tags, config.values.synchronize_flags)
52 | opts.runtime.synchronize_flags =
53 | vim.F.if_nil(opts.runtime.synchronize_flags, config.values.synchronize_flags)
54 | end
55 |
56 | function M.saved_options(opts)
57 | opts.bufname = vim.F.if_nil(opts.bufname, 'galore-saved')
58 | opts.key_bindings = keybindings(opts.key_bindings, config.values.key_bindings.saved)
59 | opts.exclude_tags = vim.F.if_nil(opts.exclude_tags, config.values.exclude_tags)
60 | opts.default_browser = vim.F.if_nil(opts.default_browser, config.values.default_browser)
61 | opts.init = vim.F.if_nil(opts.init, config.values.bufinit.saved)
62 | end
63 |
64 | local function default_view(def_view, context_value)
65 | if def_view then
66 | if def_view == 'context' then
67 | return context_value
68 | end
69 | end
70 | if config.values.default_view then
71 | if config.values.default_view == 'context' then
72 | return context_value
73 | end
74 | end
75 | return vim.F.if_nil(def_view, config.values.default_view)
76 | end
77 |
78 | local function browser_opts(opts)
79 | opts.exclude_tags = vim.F.if_nil(opts.exclude_tags, config.values.exclude_tags)
80 | opts.sort = vim.F.if_nil(opts.sort, config.values.sort)
81 | opts.limit = vim.F.if_nil(opts.limit, config.values.browser_limit)
82 | -- change this to format
83 | opts.show_message_description =
84 | vim.F.if_nil(opts.show_message_description, config.values.show_message_description)
85 | opts.emph = vim.F.if_nil(opts.emph, config.values.default_emph)
86 | runtime(opts)
87 | end
88 |
89 | function M.tmb_options(opts)
90 | opts.bufname = parse_bufname(opts.bufname, function(search)
91 | return 'galore-tmb: ' .. search
92 | end)
93 | opts.thread_reverse = vim.F.if_nil(opts.thread_reverse, config.values.thread_reverse)
94 | opts.thread_expand = vim.F.if_nil(opts.thread_expand, config.values.thread_expand)
95 | browser_opts(opts)
96 |
97 | opts.key_bindings =
98 | keybindings(opts.key_bindings, config.values.key_bindings.thread_message_browser)
99 | opts.default_view = default_view(default_view, 'message_view')
100 | opts.init = vim.F.if_nil(opts.init, config.values.bufinit.thread_message_browser)
101 | end
102 |
103 | function M.mb_options(opts)
104 | opts.bufname = parse_bufname(opts.bufname, function(search)
105 | return 'galore-messages: ' .. search
106 | end)
107 | browser_opts(opts)
108 | opts.key_bindings = keybindings(opts.key_bindings, config.values.key_bindings.message_browser)
109 | opts.default_view = default_view(default_view, 'message_view')
110 | opts.init = vim.F.if_nil(opts.init, config.values.bufinit.message_browser)
111 | end
112 |
113 | function M.threads_options(opts)
114 | opts.bufname = parse_bufname(opts.bufname, function(search)
115 | return 'galore-threads: ' .. search
116 | end)
117 | browser_opts(opts)
118 | opts.default_view = default_view(default_view, 'threads_view')
119 | opts.key_bindings = keybindings(opts.key_bindings, config.values.key_bindings.thread_browser)
120 | opts.init = vim.F.if_nil(opts.init, config.values.bufinit.thread_browser)
121 | end
122 |
123 | function M.view_options(opts)
124 | opts.bufname = parse_bufname(opts.bufname, function(filename)
125 | return filename
126 | end)
127 | opts.tag_unread = vim.F.if_nil(opts.tag_unread, config.values.tag_unread)
128 | opts.empty_tag = vim.F.if_nil(opts.empty_tag, config.values.empty_tag)
129 | opts.key_bindings = keybindings(opts.key_bindings, config.values.key_bindings.message_view)
130 | opts.key_writeback = vim.F.if_nil(opts.key_writeback, config.values.key_writeback)
131 | opts.init = vim.F.if_nil(opts.init, config.values.bufinit.message_view)
132 | end
133 |
134 | function M.thread_view_options(opts)
135 | opts.bufname = parse_bufname(opts.bufname, function(tid)
136 | return 'galore-tid:' .. tid
137 | end)
138 | opts.tag_unread = vim.F.if_nil(opts.tag_unread, config.values.tag_unread)
139 | opts.empty_tag = vim.F.if_nil(opts.empty_tag, config.values.empty_tag)
140 | opts.key_bindings = keybindings(opts.key_bindings, config.values.key_bindings.thread_view)
141 | opts.key_writeback = vim.F.if_nil(opts.key_writeback, config.values.key_writeback)
142 | opts.init = vim.F.if_nil(opts.init, config.values.bufinit.thread_view)
143 | opts.sort = vim.F.if_nil(opts.sort, config.values.sort)
144 | opts.index = vim.F.if_nil(opts.index, 1)
145 | -- add render opts here!
146 | -- can we make a way for the user to select a render?
147 | --
148 | -- extra_keys ?
149 | -- purge_empty ?
150 | end
151 |
152 | function M.render_options(opts)
153 | opts.mulilingual = vim.F.if_nil(opts.mulilingual, config.values.multilingual)
154 | opts.lang_order = vim.F.if_nil(opts.lang_order, config.values.lang_order)
155 | opts.alt_mode = vim.F.if_nil(opts.alt_mode, config.values.alt_mode)
156 | opts.alt_order = vim.F.if_nil(opts.alt_order, config.values.alt_order)
157 | opts.qoute_header = vim.F.if_nil(opts.qoute_header, config.values.qoute_header)
158 | opts.show_html = vim.F.if_nil(opts.show_html, config.values.show_html)
159 | opts.annotate_signature = vim.F.if_nil(opts.annotate_signature, config.values.annotate_signature)
160 | opts.decrypt_flags = vim.F.if_nil(opts.decrypt_flags, config.values.decrypt_flags)
161 | end
162 |
163 | function M.compose_options(opts)
164 | opts.bufname = parse_bufname(opts.bufname, function()
165 | return vim.fn.tempname()
166 | end)
167 | opts.empty_topic = fallbacks(opts.empty_topic, config.values.empty_topic, 'Empty subject')
168 | opts.key_bindings = keybindings(opts.key_bindings, config.values.key_bindings.compose)
169 | opts.pre_sent_hooks = vim.F.if_nil(opts.pre_sent_hooks, config.values.pre_sent_hooks)
170 | opts.post_sent_hooks = vim.F.if_nil(opts.post_sent_hooks, config.values.post_sent_hooks)
171 | opts.compose_headers = vim.F.if_nil(opts.compose_headers, config.values.compose_headers)
172 | opts.draft_dir = vim.F.if_nil(opts.draft_dir, config.values.draft_dir)
173 | opts.draft_tag = vim.F.if_nil(opts.draft_tag, config.values.draft_tag)
174 | opts.sent_dir = vim.F.if_nil(opts.sent_dir, config.values.sent_dir)
175 |
176 | opts.extra_headers = vim.F.if_nil(opts.extra_headers, config.values.extra_headers)
177 | --- these are builder options?
178 | opts.gpg_id = vim.F.if_nil(opts.gpg_id, config.values.gpg_id)
179 | opts.draft_encrypt = vim.F.if_nil(opts.draft_encrypt, config.values.draft_encrypt)
180 |
181 | opts.bodybuilder = vim.F.if_nil(opts.bodybuilder, builder.textbuilder)
182 | ---
183 | opts.init = vim.F.if_nil(opts.init, config.values.bufinit.compose)
184 | end
185 |
186 | return M
187 |
--------------------------------------------------------------------------------
/lua/galore/runtime.lua:
--------------------------------------------------------------------------------
1 | local config = require('galore.config')
2 | local u = require('galore.util')
3 | local nm = require('notmuch')
4 | local log = require('galore.log')
5 |
6 | local lgi = require('lgi')
7 | local gmime = lgi.require('GMime', '3.0')
8 |
9 | local runtime_dir = vim.fn.stdpath('data') .. '/galore'
10 | if os.getenv('GALOREPATH') ~= nil then
11 | runtime_dir = os.getenv('GALOREPATH')
12 | end
13 |
14 | local save_file = runtime_dir .. '/nm_saved.txt'
15 |
16 | local runtime = {}
17 |
18 | local function get(db, name)
19 | return table.concat(u.collect(nm.config_get_values_string(db, name)))
20 | end
21 |
22 | local function gets(db, name)
23 | return u.collect(nm.config_get_values_string(db, name))
24 | end
25 |
26 | function runtime.add_saved(str)
27 | local fp = io.open(save_file, 'a')
28 | if not fp then
29 | return
30 | end
31 | fp:write(str .. '\n')
32 | fp:close()
33 | end
34 |
35 | function runtime.edit_saved()
36 | if vim.fn.filereadable(save_file) ~= 0 then
37 | vim.cmd(':e ' .. save_file)
38 | end
39 | end
40 |
41 | function runtime.iterate_saved()
42 | if vim.fn.filereadable(save_file) == 0 then
43 | return function() end
44 | end
45 | return io.lines(save_file)
46 | end
47 |
48 | local function notmuch_init(path, conf, profile)
49 | local mode = 0
50 | local db = nm.db_open_with_config_raw(path, mode, conf, profile)
51 | local name = get(db, 'user.name')
52 | local primary_email = get(db, 'user.primary_email')
53 | local other_email = gets(db, 'user.other_email')
54 | local exclude_tags = gets(db, 'search.exclude_tags')
55 | local sync_flags = get(db, 'maildir.synchronize_flags')
56 | local mail_root = get(db, 'database.mail_root')
57 | local db_path = get(db, 'database.path')
58 | config.values.name = config.values.name or name
59 | config.values.synchronize_flags = vim.F.if_nil(config.values.synchronize_flags, sync_flags)
60 | config.values.primary_email = vim.F.if_nil(config.values.primary_email, primary_email)
61 | config.values.other_email = vim.F.if_nil(config.values.other_email, other_email)
62 | config.values.exclude_tags = vim.F.if_nil(config.values.exclude_tags, exclude_tags)
63 | config.values.mail_root = vim.F.if_nil(config.values.mail_root, mail_root)
64 | config.values.db_path = vim.F.if_nil(config.values.db_path, db_path)
65 | nm.db_close(db)
66 | end
67 |
68 | function runtime.with_db(func)
69 | local db = nm.db_open_with_config_raw(
70 | config.values.db_path,
71 | 0,
72 | config.values.nm_config,
73 | config.values.nm_profile
74 | )
75 | func(db)
76 | nm.db_close(db)
77 | end
78 |
79 | --- for use in async/callback code
80 | function runtime.with_db_raw(func)
81 | local db = nm.db_open_with_config_raw(
82 | config.values.db_path,
83 | 0,
84 | config.values.nm_config,
85 | config.values.nm_profile
86 | )
87 | func(db)
88 | end
89 |
90 | function runtime.with_db_writer(func)
91 | local db = nm.db_open_with_config_raw(
92 | config.values.db_path,
93 | 1,
94 | config.values.nm_config,
95 | config.values.nm_profile
96 | )
97 | local ok, err = pcall(func, db)
98 | if not ok then
99 | log.log(err, vim.log.levels.ERROR)
100 | end
101 | nm.db_close(db)
102 | end
103 |
104 | --- @param offset number
105 | --- @param error gmime.ParserWarning
106 | --- @param item string
107 | local function parser_warning(offset, error, item, _)
108 | local off = tonumber(offset)
109 | local str = safe.safestring(item) or ''
110 | local error_str = convert.show_parser_warning(error)
111 | local level = convert.parser_warning_level(error)
112 | local notification = string.format('Parsing error, %s: %s at: %d ', error_str, str, off)
113 | log.debug(notification)
114 | end
115 |
116 | local function make_gmime_parser_options()
117 | local parser_opt = gmime.ParserOptions.new()
118 | runtime.parser_opts = parser_opt
119 | end
120 |
121 | local function make_gmime_format_options()
122 | local format = gmime.FormatOptions.new()
123 |
124 | runtime.format_opts = format
125 | end
126 |
127 | function runtime.runtime_dir()
128 | return runtime_dir
129 | end
130 |
131 | function runtime.init()
132 | if vim.fn.isdirectory(runtime_dir) == 0 then
133 | if vim.fn.empty(vim.fn.glob(runtime_dir)) == 0 then
134 | error("runtime_dir exist but isn't a directory")
135 | end
136 | vim.fn.mkdir(runtime_dir, 'p', '0o700')
137 | end
138 | notmuch_init(config.values.db_path, config.values.nm_config, config.values.nm_profile)
139 | -- runtime.header_function = {}
140 | make_gmime_parser_options()
141 | make_gmime_format_options()
142 | end
143 |
144 | return runtime
145 |
--------------------------------------------------------------------------------
/lua/galore/saved.lua:
--------------------------------------------------------------------------------
1 | local nm = require('notmuch')
2 | local Buffer = require('galore.lib.buffer')
3 | local runtime = require('galore.runtime')
4 | local ordered = require('galore.lib.ordered')
5 | local o = require('galore.opts')
6 |
7 | local Saved = Buffer:new()
8 |
9 | local function make_entry(self, db, box, search, name, exclude)
10 | local q = nm.create_query(db, search)
11 | if exclude then
12 | for _, ex in ipairs(self.opts.exclude_tags) do
13 | -- we don't really care if the tags are removed or not, we want to do best effort
14 | pcall(function()
15 | nm.query_add_tag_exclude(q, ex)
16 | end)
17 | end
18 | end
19 | local unread_q = nm.create_query(db, search .. ' and tag:unread')
20 | local i = nm.query_count_messages(q)
21 | local unread_i = nm.query_count_messages(unread_q)
22 | table.insert(box, { i, unread_i, name, search })
23 | end
24 |
25 | function Saved.manual(manual)
26 | return function(self, searches)
27 | for search in ipairs(manual) do
28 | ordered.insert(searches, search.search, { search.search, search.name, false })
29 | end
30 | end
31 | end
32 |
33 | function Saved:gen_tags(searches)
34 | runtime.with_db(function(db)
35 | for tag in nm.db_get_all_tags(db) do
36 | local search = 'tag:' .. tag
37 | ordered.insert(searches, search, { search, tag, true })
38 | end
39 | end)
40 | end
41 |
42 | function Saved:gen_internal(searches)
43 | for search in runtime.iterate_saved() do
44 | ordered.insert(searches, search, { search, search, true })
45 | end
46 | end
47 |
48 | function Saved:gen_excluded(searches)
49 | for _, tag in ipairs(self.opts.exclude_tags) do
50 | local search = 'tag:' .. tag
51 | ordered.insert(searches, search, { search, tag, false })
52 | end
53 | end
54 |
55 | function Saved:get_searches()
56 | local searches = ordered.new()
57 | for _, gen in ipairs(self.searches) do
58 | gen(self, searches)
59 | end
60 | return searches
61 | end
62 |
63 | local function ppsearch(tag)
64 | local num, unread, name, search = unpack(tag)
65 | local left = string.format('%d(%d) %s', num, unread, name)
66 | return string.format('%-35s (%s)', left, search)
67 | end
68 |
69 | --- Redraw all the saved searches and update the count
70 | function Saved:refresh()
71 | self:unlock()
72 | self:clear()
73 | local box = {}
74 | local searches = self:get_searches()
75 | runtime.with_db(function(db)
76 | for _, value in ordered.pairs(searches) do
77 | make_entry(self, db, box, value[1], value[2], value[3])
78 | end
79 | end)
80 | local formated = vim.tbl_map(ppsearch, box)
81 | self:set_lines(0, 0, true, formated)
82 | self:set_lines(-2, -1, true, {})
83 | self.State = box
84 | vim.api.nvim_win_set_cursor(0, { 1, 0 })
85 | self:lock()
86 | end
87 |
88 | --- Return the currently selected line
89 | function Saved:select()
90 | local line = vim.fn.line('.')
91 | return self.State[line]
92 | end
93 |
94 | --- Create a new window for saved searches
95 | --- @param opts table {kind}
96 | --- @return any
97 | function Saved:create(opts, searches)
98 | o.saved_options(opts)
99 | return Buffer.create({
100 | name = opts.bufname,
101 | ft = 'galore-saved',
102 | kind = opts.kind,
103 | cursor = 'top',
104 | mappings = opts.key_bindings,
105 | init = function(buffer)
106 | buffer.searches = searches
107 | buffer.opts = opts
108 | buffer:refresh()
109 | opts.init(buffer)
110 | end,
111 | }, Saved)
112 | end
113 |
114 | return Saved
115 |
--------------------------------------------------------------------------------
/lua/galore/templates.lua:
--------------------------------------------------------------------------------
1 | local gu = require('galore.gmime-util')
2 | local au = require('galore.address-util')
3 |
4 | local r = require('galore.render')
5 | local config = require('galore.config')
6 | local runtime = require('galore.runtime')
7 | local u = require('galore.util')
8 | local lgi = require('lgi')
9 | local gmime = lgi.require('GMime', '3.0')
10 | local log = require('galore.log')
11 |
12 | local M = {}
13 |
14 | local function addrlist_parse(str)
15 | local ialist = gmime.InternetAddressList.parse(runtime.parser_opts, str)
16 | return ialist
17 | end
18 |
19 | -- TODO, make things more composable
20 | -- Atm we overwrite all the headers instead of merging them
21 | -- Maybe we should reparse the message before we pass,
22 | -- that way we don't have to worry about destorying it
23 |
24 | -- TODO move helper functions that could be useful for outside of this file
25 |
26 | -- Get the first none-nil value in a list of fields
27 | --- Can use non-standard fields
28 | local function get_backup(message, list)
29 | for _, v in ipairs(list) do
30 | local addr = message:get_addresses(v)
31 | if addr ~= nil and addr:length(addr) > 0 then
32 | return addr
33 | end
34 | end
35 | return nil
36 | end
37 |
38 | local function remove(list, addr)
39 | local i = 0
40 | for demail in gu.internet_address_list_iter(list) do
41 | if au.address_equal(demail, addr) then
42 | list:remove_at(i)
43 | return true
44 | end
45 | i = i + 1
46 | end
47 | return false
48 | end
49 |
50 | local function append_no_dup(addr, dst)
51 | local matched = au.ialist_contains(addr, dst)
52 | if not matched then
53 | dst:add(addr)
54 | end
55 | end
56 |
57 | local function PP(list)
58 | return list:to_string(nil, false)
59 | end
60 |
61 | local function pp(ia)
62 | return ia:to_string(nil, false)
63 | end
64 |
65 | local function safelist(...)
66 | local list = {}
67 | for i = 1, select('#', ...) do
68 | local value = select(i, ...)
69 | if value then
70 | table.insert(list, value)
71 | end
72 | end
73 | return list
74 | end
75 |
76 | local function issubscribed(addresses)
77 | local str = table.concat(config.values.mailinglist_subscribed, ', ')
78 | local list = gmime.InternetAddressList.parse(runtime.parser_opts, str)
79 | for v in gu.internet_address_list_iter(list) do
80 | if au.ialist_contains(v, addresses) then
81 | return true
82 | end
83 | end
84 | end
85 |
86 | local function get_key(gpg_id)
87 | local ctx = gmime.GpgContext.new()
88 | local mem = gmime.StreamMem.new()
89 | ctx:export_keys({ gpg_id }, mem)
90 | return mem:get_byte_array()
91 | end
92 |
93 | function M.compose_new(opts)
94 | local headers = opts.headers or {}
95 | local our = gmime.InternetAddressMailbox.new(config.values.name, config.values.primary_email)
96 | headers.From = pp(our)
97 |
98 | opts.headers = headers
99 | end
100 |
101 | --- this is wrong because we don't want to add a key as an attachments
102 | --- TODO make some default builders
103 | function M.mailkey(opts, gpg_id)
104 | local attachments = opts.attachments or {}
105 | gpg_id = gpg_id or config.values.gpg_id
106 | local key = get_key(gpg_id)
107 | table.insert(
108 | attachments,
109 | { filename = 'opengpg_pubkey.asc', data = key, mime_type = 'application/pgp-keys' }
110 | )
111 | opts.attach = attachments
112 | end
113 |
114 | function M.load_body(message, opts)
115 | local bufrender = r.new({
116 | verify = false,
117 | }, r.default_render)
118 | local buffer = {}
119 | local state = r.render_message(bufrender, message, buffer, opts)
120 | opts.Body = buffer
121 | opts.Attach = state.attachments
122 | end
123 |
124 | function M.load_headers(message, opts)
125 | opts = opts or {}
126 | local headers = {}
127 | for k, v in gu.header_iter(message) do
128 | headers[k] = v
129 | end
130 | opts.headers = headers
131 | end
132 |
133 | function M.subscribed(old_message)
134 | local to = old_message:get_to(old_message)
135 | local cc = old_message:get_cc(old_message)
136 | if issubscribed(to) or issubscribed(cc) then
137 | return true
138 | end
139 | end
140 |
141 | --- TODO clean up mft stuff
142 | function M.mft_response(old_message, opts, type)
143 | local headers = opts.headers or {}
144 | if not type or type == 'author' then
145 | local from = get_backup_header(old_message, { 'Mail-Reply-To', 'reply_to', 'from', 'sender' })
146 | headers.To = from
147 | elseif type == 'reply_all' then
148 | local mft = old_message:get_header('Mail-Followup-To')
149 | if mft ~= nil then
150 | local ialist = gmime.InternetAddressList.parse(runtime.parser_opts, mft)
151 | headers.To = PP(ialist)
152 | else
153 | M.response_message(old_message, opts, type)
154 | end
155 | end
156 | opts.headers = headers
157 | end
158 |
159 | function M.mft_insert(opts)
160 | local headers = opts.headers
161 | headers['Mail-Reply-To'] = opts.headers['Reply-To']
162 | local to = addrlist_parse(headers.To)
163 | local cc = addrlist_parse(headers.Cc)
164 | if issubscribed(to) or issubscribed(cc) then
165 | --- should we remove look and remove dups?
166 | --- because an address could be in both to and cc
167 | headers['Mail-Followup-To'] = table.concat(safelist(headers.To, headers.Cc), ',')
168 | end
169 | opts.headers = headers
170 | end
171 |
172 | function M.mft_insert_notsubbed(old_message, opts)
173 | local headers = opts.headers
174 | headers['Mail-Reply-To'] = opts.headers['Reply-To']
175 | local to = addrlist_parse(headers.To)
176 | local cc = addrlist_parse(headers.Cc)
177 | local ml = old_message:get_header('List-Post')
178 | if ml ~= nil and not (issubscribed(to) or issubscribed(cc)) then
179 | ml = PP(ml)
180 | headers['Mail-Followup-To'] = table.concat(safelist(headers.From, ml), ',')
181 | end
182 | opts.headers = headers
183 | end
184 |
185 | function M.smart_response(old_message, opts, backup_type)
186 | local ml = old_message:get_header('List-Post')
187 | if ml then
188 | M.response_message(old_message, opts, 'mailinglist')
189 | return
190 | end
191 | M.response_message(old_message, opts, backup_type)
192 | end
193 |
194 | function M.response_message(old_message, opts, type)
195 | local at = gmime.AddressType
196 | local headers = opts.headers or {}
197 |
198 | local addr = au.get_our_email(old_message)
199 | local our = gmime.InternetAddressMailbox.new(config.values.name, addr)
200 | local our_str = pp(our)
201 |
202 | local sub = old_message:get_subject()
203 | headers.Subject = u.add_prefix(sub, 'Re:')
204 |
205 | headers.From = our_str
206 |
207 | local from = get_backup(old_message, { at.REPLY_TO, at.FROM, at.SENDER }):get_address(0)
208 | if not type or type == 'reply' then
209 | headers.To = pp(from)
210 | elseif type == 'reply_all' then
211 | --- these are destructive
212 | local to = old_message:get_addresses(at.TO)
213 | append_no_dup(from, to)
214 | remove(to, our)
215 | headers.To = PP(to)
216 |
217 | local cc = old_message:get_addresses(at.CC)
218 | remove(to, our)
219 | headers.Cc = PP(cc)
220 |
221 | local bcc = old_message:get_addresses(at.BCC)
222 | remove(to, our)
223 | headers.Bcc = PP(bcc)
224 | elseif type == 'mailinglist' then
225 | local ml = old_message:get_header('List-Post')
226 | headers.To = u.unmailto(ml)
227 | end
228 | opts.headers = headers
229 | end
230 |
231 | function M.forward_resent(old_message, to_str, opts)
232 | local at = gmime.AddressType
233 | local headers = opts.headers or {}
234 |
235 | local addr = au.get_our_email(old_message)
236 | local our = gmime.InternetAddressMailbox.new(config.values.name, addr)
237 | local our_str = pp(our)
238 | headers.From = our_str
239 |
240 | local sub = old_message:get_subject()
241 | sub = u.add_prefix(sub, 'FWD:')
242 | headers.Subject = sub
243 |
244 | headers.To = to_str
245 |
246 | opts.headers = headers
247 |
248 | headers['Resent-To'] = PP(old_message:get_address(at.TO))
249 | headers['Resent-From'] = PP(old_message:get_address(at.FROM))
250 | headers['Resent-Cc'] = PP(old_message:get_address(at.CC))
251 | headers['Resent-Bcc'] = PP(old_message:get_address(at.BCC))
252 | headers['Recent-Date'] = old_message:get_date()
253 | headers['Recent-Id'] = old_message:get_message_id()
254 | -- insert before the body
255 | table.insert(opts.Body, 1, { '--- Forwarded message ---' })
256 | opts.headers = headers
257 | end
258 |
259 | function M.bounce(old_message, opts)
260 | local at = gmime.AddressType
261 | local from = get_backup(old_message, { at.REPLY_TO, at.FROM, at.SENDER }):get_address(0)
262 | M.forward_resent(old_message, from, opts)
263 |
264 | local sub = old_message:get_subject()
265 | sub = u.add_prefix(sub, 'Return:')
266 | opts.headers.Subject = sub
267 | table.remove(opts.Body, 1)
268 | table.insert(opts.Body, 1, { "--- This email isn't for me ---" })
269 | opts.attachments = {} -- do not bounce the attachments
270 | end
271 |
272 | function M.Resent(old_message, opts)
273 | local at = gmime.AddressType
274 | local headers = opts.headers or {}
275 |
276 | headers.To = PP(old_message:get_address(at.TO))
277 | headers.From = PP(old_message:get_address(at.FROM))
278 | headers.Cc = PP(old_message:get_address(at.CC))
279 | headers.Bcc = PP(old_message:get_address(at.BCC))
280 | headers.Subject = old_message:get_subject()
281 |
282 | opts.headers = headers
283 | end
284 |
285 | function M.subscribe(old_message, opts)
286 | local unsub = old_message:get_header('List-Subscribe')
287 | if unsub == nil then
288 | log.log_err('Subscribe header not found')
289 | return
290 | end
291 | local addr = au.get_our_email(old_message)
292 | local headers = opts.headers or {}
293 | headers.From = { config.values.name, addr }
294 | headers.To = u.unmailto(unsub)
295 | headers.Subject = 'Subscribe'
296 | opts.headers = headers
297 | end
298 |
299 | function M.unsubscribe(old_message, opts)
300 | local unsub = old_message:get_header('List-Unsubscribe')
301 | if unsub == nil then
302 | log.log_err('Subscribe header not found')
303 | return
304 | end
305 | local addr = au.get_our_email(old_message)
306 | local headers = opts.headers or {}
307 | headers.From = { config.values.name, addr }
308 | headers.To = u.unmailto(unsub)
309 | headers.Subject = 'Unsubscribe'
310 | opts.headers = headers
311 | end
312 |
313 | return M
314 |
--------------------------------------------------------------------------------
/lua/galore/test_utils.lua:
--------------------------------------------------------------------------------
1 | -- move this to tests
2 | local Job = require('plenary.job')
3 | local galore = require('galore')
4 |
5 | local M = {}
6 | -- lets do it like this for now
7 | -- local test_path = (function()
8 | -- local dirname = string.sub(debug.getinfo(1).source, 2, #"/test_utils.lua" * -1)
9 | -- return dirname .. "/../../"
10 | -- end)()
11 |
12 | local test_path = os.getenv('GALORETESTDATA')
13 |
14 | if not test_path then
15 | error('env GALORETESTDATA not set')
16 | end
17 |
18 | local script = test_path .. '/nm_init.sh'
19 |
20 | function M.setup(testname)
21 | local config_path = test_path .. testname .. '/notmuch/notmuch-config'
22 | Job:new({
23 | command = script,
24 | args = { testname },
25 | }):sync()
26 | galore.setup({
27 | nm_config = config_path,
28 | })
29 | galore.connect()
30 | end
31 |
32 | function M.cleanup(testname)
33 | local test = test_path .. testname
34 | vim.fn.delete(test, 'rf')
35 | end
36 |
37 | function M.notmuch_random_message() end
38 |
39 | function M.gmime_random_message() end
40 |
41 | return M
42 |
--------------------------------------------------------------------------------
/lua/galore/thread_browser.lua:
--------------------------------------------------------------------------------
1 | local o = require('galore.opts')
2 | local async = require('plenary.async')
3 | local Buffer = require('galore.lib.buffer')
4 | local browser = require('galore.browser')
5 |
6 | local Threads = Buffer:new()
7 |
8 | local function threads_get(self)
9 | local first = true
10 | self.highlight = {}
11 | return browser.get_entries(self, 'show-thread', function(thread, n)
12 | if thread then
13 | table.insert(self.State, thread.id)
14 | if first then
15 | vim.api.nvim_buf_set_lines(self.handle, 0, -1, false, { thread.entry })
16 | first = false
17 | else
18 | vim.api.nvim_buf_set_lines(self.handle, -1, -1, false, { thread.entry })
19 | end
20 | if thread.highlight then
21 | table.insert(self.highlight, n)
22 | vim.api.nvim_buf_add_highlight(self.handle, self.dians, 'GaloreHighlight', n, 0, -1)
23 | end
24 | return 1
25 | end
26 | end)
27 | end
28 |
29 | function Threads:async_runner()
30 | self.updating = true
31 | local func = async.void(function()
32 | self.runner = threads_get(self)
33 | pcall(function()
34 | self.runner.resume(self.opts.limit)
35 | self:lock()
36 | self.updating = false
37 | end)
38 | end)
39 | func()
40 | end
41 |
42 | function Threads:refresh()
43 | if self.runner then
44 | self.runner.close()
45 | self.runner = nil
46 | end
47 | self:unlock()
48 | self:clear()
49 | self:async_runner()
50 | end
51 |
52 | function Threads:update(line_nr)
53 | local id = self.State[line_nr]
54 | browser.update_lines_helper(self, 'show-thread', 'thread:' .. id, line_nr)
55 | end
56 |
57 | function Threads:commands() end
58 |
59 | function Threads:select_thread()
60 | local line = vim.api.nvim_win_get_cursor(0)[1]
61 | return line, self.State[line]
62 | end
63 |
64 | function Threads:create(search, opts)
65 | o.threads_options(opts)
66 | Buffer.create({
67 | name = opts.bufname(search),
68 | ft = 'galore-browser',
69 | kind = opts.kind,
70 | cursor = 'top',
71 | parent = opts.parent,
72 | mappings = opts.key_bindings,
73 | init = function(buffer)
74 | buffer.opts = opts
75 | buffer.search = search
76 | buffer.dians = vim.api.nvim_create_namespace('galore-dia')
77 | buffer:refresh()
78 | buffer:commands()
79 | if opts.limit then
80 | browser.scroll(buffer)
81 | end
82 | opts.init(buffer)
83 | end,
84 | }, Threads)
85 | end
86 |
87 | return Threads
88 |
--------------------------------------------------------------------------------
/lua/galore/thread_message_browser.lua:
--------------------------------------------------------------------------------
1 | local Buffer = require('galore.lib.buffer')
2 | local o = require('galore.opts')
3 | local async = require('plenary.async')
4 | local browser = require('galore.browser')
5 |
6 | local Tmb = Buffer:new()
7 |
8 | local function tmb_get(self)
9 | local first = true
10 | self.highlight = {}
11 | return browser.get_entries(self, 'show-tree', function(thread, n)
12 | local i = 0
13 | for _, message in ipairs(thread) do
14 | table.insert(self.State, message.id)
15 | if first then
16 | vim.api.nvim_buf_set_lines(self.handle, 0, -1, false, { message.entry })
17 | first = false
18 | else
19 | vim.api.nvim_buf_set_lines(self.handle, -1, -1, false, { message.entry })
20 | end
21 | if message.highlight then
22 | local idx = i + n - 1
23 | table.insert(self.highlight, idx)
24 | vim.api.nvim_buf_add_highlight(self.handle, self.dians, 'GaloreHighlight', idx, 0, -1)
25 | end
26 | i = i + 1
27 | end
28 | -- end
29 | -- We need to api for folds etc and
30 | -- we don't want dump all off them like this
31 | -- but otherwise this works
32 | -- if #thread > 1 then
33 | -- local threadinfo = {
34 | -- stop = i-1,
35 | -- start = linenr,
36 | -- }
37 | -- table.insert(self.threads, threadinfo)
38 | -- end
39 | return i
40 | end)
41 | end
42 |
43 | function Tmb:async_runner()
44 | self.updating = true
45 | self.dias = {}
46 | self.threads = {}
47 | local func = async.void(function()
48 | self.runner = tmb_get(self)
49 | pcall(function()
50 | self.runner.resume(self.opts.limit)
51 | self:lock()
52 | self.updating = false
53 | end)
54 | end)
55 | func()
56 | end
57 |
58 | --- Redraw the whole window
59 | function Tmb:refresh()
60 | if self.runner then
61 | self.runner.close()
62 | self.runner = nil
63 | end
64 | self:unlock()
65 | self:clear()
66 | self:async_runner()
67 | end
68 |
69 | -- have an autocmd for refresh?
70 | function Tmb:trigger_refresh()
71 | -- trigger an refresh in autocmd
72 | end
73 |
74 | function Tmb:update(line_nr)
75 | local id = self.State[line_nr]
76 | browser.update_lines_helper(self, 'show-single-tree', 'id:' .. id, line_nr)
77 | end
78 |
79 | function Tmb:commands()
80 | vim.api.nvim_buf_create_user_command(self.handle, 'GaloreChangetag', function(args)
81 | if args.args then
82 | local callback = require('galore.callback')
83 | callback.change_tag(self, args)
84 | end
85 | end, {
86 | nargs = '*',
87 | })
88 | end
89 |
90 | -- function Tmb:autocmd()
91 |
92 | --- Create a browser grouped by threads
93 | --- @param search string a notmuch search string
94 | --- @param opts table
95 | --- @return any
96 | function Tmb:create(search, opts)
97 | o.tmb_options(opts)
98 | return Buffer.create({
99 | name = opts.bufname(search),
100 | ft = 'galore-browser',
101 | kind = opts.kind,
102 | cursor = 'top',
103 | parent = opts.parent,
104 | mappings = opts.key_bindings,
105 | init = function(buffer)
106 | buffer.search = search
107 | buffer.opts = opts
108 | buffer.diagnostics = {}
109 | buffer.dians = vim.api.nvim_create_namespace('galore-dia')
110 | buffer:refresh(search)
111 | buffer:commands()
112 | if opts.limit then
113 | browser.scroll(buffer)
114 | end
115 | opts.init(buffer)
116 | end,
117 | }, Tmb)
118 | end
119 |
120 | return Tmb
121 |
--------------------------------------------------------------------------------
/lua/galore/thread_view.lua:
--------------------------------------------------------------------------------
1 | local r = require('galore.render')
2 | local u = require('galore.util')
3 | local Buffer = require('galore.lib.buffer')
4 | local nu = require('galore.notmuch-util')
5 | -- local nm = require("galore.notmuch")
6 | local nm = require('notmuch')
7 | local ui = require('galore.ui')
8 | local runtime = require('galore.runtime')
9 | local o = require('galore.opts')
10 | local gu = require('galore.gmime-util')
11 |
12 | local Thread = Buffer:new()
13 |
14 | function Thread:update(tid)
15 | self:unlock()
16 | self:clear()
17 | self.thread_parts = {}
18 | self.states = {}
19 | self.lines = {}
20 |
21 | runtime.with_db(function(db)
22 | local query = nm.create_query(db, 'thread:' .. tid)
23 | nm.query_set_sort(query, self.opts.sort)
24 | local i = 1
25 | local tot = nm.query_count_messages(query)
26 | for nm_message in nm.query_get_messages(query) do
27 | local message_start = vim.fn.line('$')
28 |
29 | local line = nu.get_message(nm_message)
30 | line.total = tot
31 | line.index = i
32 |
33 | local message = gu.parse_message(line.filenames[1])
34 |
35 | local buffer = {}
36 | r.show_headers(message, self.handle, { ns = self.ns }, line, message_start)
37 | local body = vim.fn.line('$')
38 | local state = r.render_message(r.default_render, message, buffer, {
39 | offset = body - 1,
40 | keys = line.keys,
41 | })
42 | table.insert(self.states, state)
43 | table.insert(self.lines, line)
44 | u.purge_empty(buffer)
45 | self:set_lines(-1, -1, true, buffer)
46 | local message_stop = vim.fn.line('$')
47 | if not vim.tbl_isempty(state.attachments) then
48 | ui.render_attachments(state.attachments, message_stop - 1, self.handle, self.ns)
49 | end
50 | table.insert(
51 | self.thread_parts,
52 | { start = message_start, stop = message_stop, body = body, mid = line.id }
53 | )
54 | i = i + 1
55 | end
56 | self:set_lines(0, 1, true, {})
57 | -- vim.schedule(function ()
58 | -- for idx, state in ipairs(self.states) do
59 | -- for _, cb in ipairs(state.callbacks) do
60 | -- cb(self.handle, self.ns)
61 | -- end
62 | -- self.states[idx] = nil
63 | -- end
64 | -- end)
65 | end)
66 |
67 | self:lock()
68 | end
69 |
70 | function Thread:with_all_attachments(func, ...)
71 | local attachments = {}
72 | for _, state in ipairs(self.states) do
73 | vim.list_extend(attachments, state.attachments)
74 | end
75 | func(attachments, ...)
76 | end
77 |
78 | function Thread:with_selected_attachments(func, ...)
79 | local _, i = self:get_selected()
80 | local attachments = self.states[i].attachments
81 | func(attachments, ...)
82 | end
83 |
84 | function Thread:get_selected()
85 | local line = unpack(vim.api.nvim_win_get_cursor(0))
86 | line = line + 1
87 | for i, m in ipairs(self.thread_parts) do
88 | if m.start <= line and m.stop >= line then
89 | return m.mid, i
90 | end
91 | end
92 | end
93 |
94 | function Thread:message_view()
95 | local mid = self:get_selected()
96 | local mw = require('galore.message_view')
97 | local opts = o.bufcopy(self.opts)
98 | mw:create(mid, opts)
99 | end
100 |
101 | function Thread:redraw(line)
102 | self:focus()
103 | self:update(line)
104 | end
105 |
106 | function Thread:set(i)
107 | local _, col = unpack(vim.api.nvim_win_get_cursor(0))
108 | for j, m in ipairs(self.thread_parts) do
109 | if i == j then
110 | vim.api.nvim_win_set_cursor(0, { m.start, col })
111 | end
112 | end
113 | end
114 |
115 | function Thread:next()
116 | local found = false
117 | local line, col = unpack(vim.api.nvim_win_get_cursor(0))
118 | for _, m in ipairs(self.thread_parts) do
119 | if m.start <= line and m.stop >= line then
120 | vim.api.nvim_win_set_cursor(0, { m.start, col })
121 | found = true
122 | elseif found then
123 | vim.api.nvim_win_set_cursor(0, { m.start, col })
124 | return
125 | end
126 | end
127 | end
128 |
129 | function Thread:prev()
130 | local line, col = unpack(vim.api.nvim_win_get_cursor(0))
131 | for i, m in ipairs(self.thread_parts) do
132 | if m and m.start <= line and m.stop >= line then
133 | vim.api.nvim_win_set_cursor(0, { m.start, col })
134 | return
135 | end
136 | end
137 | end
138 |
139 | -- local function verify_signatures(self)
140 | -- local state = {}
141 | -- local function verify(_, part, _)
142 | -- if gp.is_multipart_signed(part) then
143 | -- local verified = gcu.verify_signed(part)
144 | -- if state.verified == nil then
145 | -- state.verified = verified
146 | -- end
147 | -- state.verified = state.verified and verified
148 | -- end
149 | -- end
150 | -- if not self.message then
151 | -- return
152 | -- end
153 | -- gp.message_foreach_dfs(self.message, verify)
154 | -- return state.verified or state.verified == nil
155 | -- end
156 |
157 | function Thread:commands()
158 | -- vim.api.nvim_buf_create_user_command(self.handle, "GaloreSaveAttachment", function (args)
159 | -- if args.fargs then
160 | -- local save_path = "."
161 | -- if #args.fargs > 2 then
162 | -- save_path = args.fargs[2]
163 | -- end
164 | -- save_attachment(self.state.attachments, args.fargs[1], save_path)
165 | -- end
166 | -- end, {
167 | -- nargs = "*",
168 | -- complete = function ()
169 | -- local files = {}
170 | -- for _, v in ipairs(self.state.attachments) do
171 | -- table.insert(files, v.filename)
172 | -- end
173 | -- return files
174 | -- end,
175 | -- })
176 | -- vim.api.nvim_buf_create_user_command(self.handle, "GaloreVerify", function ()
177 | -- print(verify_signatures(self))
178 | -- end, {
179 | -- })
180 | end
181 |
182 | function Thread:create(tid, opts)
183 | o.thread_view_options(opts)
184 | Buffer.create({
185 | name = opts.bufname(tid),
186 | ft = 'mail',
187 | kind = opts.kind,
188 | parent = opts.parent,
189 | mappings = opts.key_bindings,
190 | init = function(buffer)
191 | buffer.opts = opts
192 | buffer.vline = opts.vline
193 | buffer.ns = vim.api.nvim_create_namespace('galore-thread-view')
194 | buffer.dians = vim.api.nvim_create_namespace('galore-dia')
195 | -- mark_read(buffer, opts.parent, line, opts.vline)
196 | buffer:update(tid)
197 | buffer:commands()
198 | buffer:set(opts.index)
199 | opts.init(buffer)
200 | end,
201 | }, Thread)
202 | end
203 |
204 | function Thread.open_attach() end
205 |
206 | return Thread
207 |
--------------------------------------------------------------------------------
/lua/galore/ui.lua:
--------------------------------------------------------------------------------
1 | local M = {}
2 |
3 | --- Adds the attachments in the bottom of the buffer
4 | function M.render_attachments(attachments, line, buffer, ns)
5 | if #attachments == 0 then
6 | return
7 | end
8 | local marks = {}
9 | for _, v in ipairs(attachments) do
10 | local str = string.format('- [%s]', v.filename)
11 | table.insert(marks, { str, 'GaloreAttachment' })
12 | end
13 | local opts = {
14 | virt_lines = {
15 | marks,
16 | },
17 | }
18 | vim.api.nvim_buf_set_extmark(buffer, ns, line, 0, opts)
19 | end
20 |
21 | function M.extmark(buf, ns, style, text, line)
22 | -- for now
23 | if not ns then
24 | return
25 | end
26 | local opts = {
27 | virt_lines = {
28 | { { text, style } },
29 | },
30 | }
31 | vim.api.nvim_buf_set_extmark(buf, ns, line, 0, opts)
32 | end
33 |
34 | return M
35 |
--------------------------------------------------------------------------------
/lua/galore/url.lua:
--------------------------------------------------------------------------------
1 | local function firstToUpper(str)
2 | return (str:gsub('^%l', string.upper))
3 | end
4 |
5 | local function decode(str)
6 | local function hex_to_char(hex)
7 | return string.char(tonumber(hex, 16))
8 | end
9 | return str:gsub('%%([a-fA-F0-9][a-fA-F0-9])', hex_to_char)
10 | end
11 |
12 | local function parse_mailto(str)
13 | if type(str) ~= 'string' then
14 | vim.api.nvim_err_write('Argument should be a string')
15 | return
16 | end
17 | local values = {}
18 |
19 | str = str:gsub('mailto:', '')
20 | if string.gmatch(str, '?') then
21 | local lp = str:gsub('.*?', '')
22 | for k, v in string.gmatch(lp, '([^&=?]+)=([^&=?]+)') do
23 | k = firstToUpper(k)
24 | values[k] = decode(v)
25 | end
26 | end
27 | local to = decode(str:gsub('?.*', ''))
28 | --- dunno if we should concat these
29 | if to and to ~= '' then
30 | if values['To'] then
31 | values['To'] = to .. ', ' .. values['To']
32 | else
33 | values['To'] = to
34 | end
35 | end
36 | return values
37 | end
38 |
39 | local function normalize(tbl)
40 | return { To = tbl.To, Cc = tbl.Cc, Subject = tbl.Subject, Attach = tbl.Attach, Body = tbl.Body }
41 | end
42 |
43 | return {
44 | parse_mailto,
45 | normalize,
46 | }
47 |
--------------------------------------------------------------------------------
/lua/galore/util.lua:
--------------------------------------------------------------------------------
1 | local M = {}
2 |
3 | function M.trim(s)
4 | return s:gsub('^%s*(.-)%s*$', '%1')
5 | end
6 |
7 | function M.id(arg)
8 | return arg
9 | end
10 |
11 | function M.make_keys(iter)
12 | local box = {}
13 | for v in iter do
14 | box[v] = true
15 | end
16 | return box
17 | end
18 |
19 | --- @param t table
20 | --- @param sep string
21 | --- @param i? number
22 | --- @param j? number
23 | function M.keys_concat(t, sep, i, j)
24 | return table.concat(vim.tbl_keys(t), sep, i, j)
25 | end
26 |
27 | function M.contains(list, item)
28 | for _, l in ipairs(list) do
29 | if l == item then
30 | return true
31 | end
32 | end
33 | return false
34 | end
35 |
36 | function M.string_setlength(str, len)
37 | local trimmed = vim.fn.strcharpart(str, 0, len)
38 | local tlen = vim.fn.strchars(trimmed)
39 | return trimmed .. string.rep(' ', len - tlen)
40 | end
41 |
42 | function M.reverse(list)
43 | local box = {}
44 | for i = #list, 1, -1 do
45 | box[#box + 1] = list[i]
46 | end
47 | return box
48 | end
49 |
50 | function M.upairs(list)
51 | local i = 1
52 | return function()
53 | if i < #list then
54 | local element = list[i]
55 | i = i + 1
56 | return element
57 | end
58 | end
59 | end
60 |
61 | -- Go over any iterator and just put all the values in an array
62 | -- This can be slow but great for toying around
63 | --- @param it function iterator to loop over
64 | --- @param t? table
65 | --- @param i? number index
66 | --- @return array
67 | function M.collect(it, t, i)
68 | local box = {}
69 | if t == nil then
70 | for v in it do
71 | table.insert(box, v)
72 | end
73 | return box
74 | else
75 | for _, v in it, t, i do
76 | table.insert(box, v)
77 | end
78 | return box
79 | end
80 | end
81 |
82 | function M.add_prefix(str, prefix)
83 | if not vim.startswith(str, prefix) then
84 | str = prefix .. ' ' .. str
85 | end
86 | return str
87 | end
88 |
89 | function M.basename(path)
90 | return string.gsub(path, '.*/(.*)', '%1')
91 | end
92 |
93 | function M.is_absolute(path)
94 | return path:sub(1, 1) == '/'
95 | end
96 |
97 | function M.save_path(path, default_path)
98 | path = vim.fn.expand(path)
99 | default_path = default_path or ''
100 | if not M.is_absolute(path) then
101 | path = default_path .. path
102 | end
103 | return path
104 | end
105 |
106 | function M.gen_name(name, num)
107 | if num == 1 then
108 | return name
109 | end
110 | return string.format('%s-%d', name, num)
111 | end
112 |
113 | function M.format(part, qoute)
114 | local box = {}
115 | for line in string.gmatch(part, '[^\n]+') do
116 | table.insert(box, line)
117 | end
118 | return box
119 | end
120 |
121 | function M.purge_empty(list)
122 | for i, v in ipairs(list) do
123 | if v == '' then
124 | table.remove(list, i)
125 | else
126 | break
127 | end
128 | end
129 |
130 | --- remove any empty line at the end of the list
131 | local stop = #list
132 | while stop > 0 do
133 | if list[stop] == '' then
134 | table.remove(list, stop)
135 | stop = stop - 1
136 | else
137 | break
138 | end
139 | end
140 | end
141 |
142 | function M.unmailto(addr)
143 | return addr:gsub('', '%1')
144 | end
145 |
146 | return M
147 |
--------------------------------------------------------------------------------
/lua/galore/views.lua:
--------------------------------------------------------------------------------
1 | local Path = require('plenary.path')
2 | local gu = require('galore.gmime-util')
3 | local runtime = require('galore.runtime')
4 | local M = {}
5 |
6 | --- TODO
7 |
8 | function M.save_attachment(attachments, idx, save_path, overwrite)
9 | if attachments[idx] then
10 | local filename = attachments[idx].filename
11 | local path = Path:new(save_path)
12 | if path:is_dir() then
13 | path = path:joinpath(filename)
14 | end
15 | if path:exists() and not overwrite then
16 | error('file exists')
17 | return
18 | end
19 | gu.save_part(attachments[idx].part, path:expand())
20 | return
21 | end
22 | vim.api.nvim_err_writeln('No attachment with that name')
23 | end
24 |
25 | function M.select_attachment(attachments, cb)
26 | local files = {}
27 | for _, v in ipairs(attachments) do
28 | table.insert(files, v.filename)
29 | end
30 | vim.ui.select(files, {
31 | prompt = 'Select attachment: ',
32 | }, function(item, idx)
33 | if item then
34 | cb(attachments[idx])
35 | else
36 | vim.api.nvim_err_writeln('No file selected')
37 | end
38 | end)
39 | end
40 |
41 | local function mark_read(self, pb, line, vline)
42 | runtime.with_db_writer(function(db)
43 | self.opts.tag_unread(db, line.id)
44 | nu.tag_if_nil(db, line.id, self.opts.empty_tag)
45 | --- this doesn't work because we can't redraw just a message but the whole thread?
46 | --- Maybe redraw the whole thread?
47 | nu.update_line(db, pb, line, vline)
48 | end)
49 | end
50 |
51 | return M
52 |
--------------------------------------------------------------------------------
/meson.build:
--------------------------------------------------------------------------------
1 | project('galore', 'c',
2 | version : '0.1',
3 | default_options : ['warning_level=3'])
4 | subdir('src')
5 |
--------------------------------------------------------------------------------
/plugin/galore.lua:
--------------------------------------------------------------------------------
1 | if 1 ~= vim.fn.has "nvim-0.7.0" then
2 | vim.api.nvim_err_writeln "Galore requires at least nvim-0.7.0."
3 | return
4 | end
5 |
6 | if vim.g.loaded_galore == 1 then
7 | return
8 | end
9 | vim.g.loaded_galore = 1
10 |
11 | vim.api.nvim_set_hl(0, "GaloreVerifyGreen", {fg="Green"})
12 | vim.api.nvim_set_hl(0, "GaloreVerifyRed", {fg="Red"})
13 | vim.api.nvim_set_hl(0, "GaloreSeperator", {fg="Red"})
14 | vim.api.nvim_set_hl(0, "GaloreAttachment", {fg="Red"})
15 | vim.api.nvim_set_hl(0, "GaloreHeader", {link="Comment"})
16 | vim.api.nvim_set_hl(0, "GaloreVerifyGreen", {fg="Green"})
17 | vim.api.nvim_set_hl(0, "GaloreVerifyRed", {fg="Red"})
18 |
19 | vim.api.nvim_create_user_command("Galore", function (args)
20 | local opts = {}
21 | if args.args and args.args ~= "" then
22 | opts.search = args.args
23 | end
24 | require('galore').open(opts)
25 | end, {
26 | nargs = "*",
27 | })
28 |
29 | vim.api.nvim_create_user_command("GaloreCompose", function (args)
30 | require('galore').compose("replace", args.args)
31 | end, {
32 | nargs = "?",
33 | })
34 |
35 | vim.api.nvim_create_user_command("GaloreMailto", function (args)
36 | require('galore').mailto("replace", args.args)
37 | end, {
38 | nargs = "?",
39 | })
40 |
41 | vim.api.nvim_create_user_command("GaloreNew", function ()
42 | require('galore.jobs').new()
43 | end, {
44 | nargs = 0,
45 | })
46 |
--------------------------------------------------------------------------------
/res/galore.svg:
--------------------------------------------------------------------------------
1 |
2 |
207 |
--------------------------------------------------------------------------------
/res/message_view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dagle/galore/0fe4c74186d18e6449b1be951564c5392cf970d2/res/message_view.png
--------------------------------------------------------------------------------
/res/overview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dagle/galore/0fe4c74186d18e6449b1be951564c5392cf970d2/res/overview.png
--------------------------------------------------------------------------------
/res/saved.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dagle/galore/0fe4c74186d18e6449b1be951564c5392cf970d2/res/saved.png
--------------------------------------------------------------------------------
/res/telescope.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dagle/galore/0fe4c74186d18e6449b1be951564c5392cf970d2/res/telescope.png
--------------------------------------------------------------------------------
/res/thread_message.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dagle/galore/0fe4c74186d18e6449b1be951564c5392cf970d2/res/thread_message.png
--------------------------------------------------------------------------------
/src/galore-autocrypt.c:
--------------------------------------------------------------------------------
1 | /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 | /* GMime
3 | * Copyright (C) 2000-2020 Jeffrey Stedfast
4 | *
5 | * This library is free software; you can redistribute it and/or
6 | * modify it under the terms of the GNU Lesser General Public License
7 | * as published by the Free Software Foundation; either version 2.1
8 | * of the License, or (at your option) any later version.
9 | *
10 | * This library is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 | * Lesser General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Lesser General Public
16 | * License along with this library; if not, write to the Free
17 | * Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
18 | * 02110-1301, USA.
19 | */
20 |
21 |
22 | #ifdef HAVE_CONFIG_H
23 | #include
24 | #endif
25 |
26 | #include
27 | #include
28 | #include
29 |
30 | #include "galore-autocrypt.h"
31 | #include
32 | #include
33 | #include
34 | #include
35 | #include
36 | #include
37 | #include
38 | #include "galore-gpgme-utils.h"
39 |
40 | #ifdef ENABLE_DEBUG
41 | #define d(x) x
42 | #else
43 | #define d(x)
44 | #endif
45 |
46 | #define _(x) x
47 |
48 |
49 | /**
50 | * SECTION: gmime-gpg-context
51 | * @title: GaloreAutoCryptContext
52 | * @short_description: GnuPG crypto contexts
53 | * @see_also: #GMimeCryptoContext
54 | *
55 | * A #GaloreAutoCryptContext is a #GMimeCryptoContext that uses GnuPG to do
56 | * all of the encryption and digital signatures.
57 | **/
58 |
59 |
60 | /**
61 | * GaloreAutoCryptContext:
62 | *
63 | * A GnuPG crypto context.
64 | **/
65 | struct _GaloreAutoCryptContext {
66 | GMimeCryptoContext parent_object;
67 |
68 | gpgme_ctx_t ctx;
69 | };
70 |
71 | struct _GaloreAutoCryptContextClass {
72 | GMimeCryptoContextClass parent_class;
73 |
74 | };
75 |
76 |
77 | static void galore_au_context_class_init (GaloreAutoCryptContextClass *klass);
78 | static void galore_au_context_init (GaloreAutoCryptContext *ctx, GaloreAutoCryptContextClass *klass);
79 | static void galore_au_context_finalize (GObject *object);
80 |
81 | static GMimeDigestAlgo au_digest_id (GMimeCryptoContext *ctx, const char *name);
82 | static const char *au_digest_name (GMimeCryptoContext *ctx, GMimeDigestAlgo digest);
83 |
84 | static int au_sign (GMimeCryptoContext *ctx, gboolean detach, const char *userid,
85 | GMimeStream *istream, GMimeStream *ostream, GError **err);
86 |
87 | static const char *au_get_signature_protocol (GMimeCryptoContext *ctx);
88 | static const char *au_get_encryption_protocol (GMimeCryptoContext *ctx);
89 | static const char *au_get_key_exchange_protocol (GMimeCryptoContext *ctx);
90 |
91 | static GMimeSignatureList *au_verify (GMimeCryptoContext *ctx, GMimeVerifyFlags flags,
92 | GMimeStream *istream, GMimeStream *sigstream,
93 | GMimeStream *ostream, GError **err);
94 |
95 | static int au_encrypt (GMimeCryptoContext *ctx, gboolean sign, const char *userid, GMimeEncryptFlags flags,
96 | GPtrArray *recipients, GMimeStream *istream, GMimeStream *ostream, GError **err);
97 |
98 | static GMimeDecryptResult *au_decrypt (GMimeCryptoContext *ctx, GMimeDecryptFlags flags, const char *session_key,
99 | GMimeStream *istream, GMimeStream *ostream, GError **err);
100 |
101 | static int au_import_keys (GMimeCryptoContext *ctx, GMimeStream *istream, GError **err);
102 |
103 | static int au_export_keys (GMimeCryptoContext *ctx, const char *keys[],
104 | GMimeStream *ostream, GError **err);
105 |
106 |
107 | static GMimeCryptoContextClass *parent_class = NULL;
108 | static char *path = NULL;
109 |
110 |
111 | GType
112 | galore_au_context_get_type ()
113 | {
114 | static GType type = 0;
115 |
116 | if (!type) {
117 | static const GTypeInfo info = {
118 | sizeof (GaloreAutoCryptContextClass),
119 | NULL, /* base_class_init */
120 | NULL, /* base_class_finalize */
121 | (GClassInitFunc) galore_au_context_class_init,
122 | NULL, /* class_finalize */
123 | NULL, /* class_data */
124 | sizeof (GaloreAutoCryptContext),
125 | 0, /* n_preallocs */
126 | (GInstanceInitFunc) galore_au_context_init,
127 | };
128 |
129 | type = g_type_register_static (GMIME_TYPE_CRYPTO_CONTEXT, "GMimeAutoCryptContext", &info, 0);
130 | }
131 |
132 | return type;
133 | }
134 |
135 |
136 | static void
137 | galore_au_context_class_init (GaloreAutoCryptContextClass *klass)
138 | {
139 | GObjectClass *object_class = G_OBJECT_CLASS (klass);
140 | GMimeCryptoContextClass *crypto_class = GMIME_CRYPTO_CONTEXT_CLASS (klass);
141 |
142 | parent_class = g_type_class_ref (G_TYPE_OBJECT);
143 |
144 | object_class->finalize = galore_au_context_finalize;
145 |
146 | crypto_class->digest_id = au_digest_id;
147 | crypto_class->digest_name = au_digest_name;
148 | crypto_class->sign = au_sign;
149 | crypto_class->verify = au_verify;
150 | crypto_class->encrypt = au_encrypt;
151 | crypto_class->decrypt = au_decrypt;
152 | crypto_class->import_keys = au_import_keys;
153 | crypto_class->export_keys = au_export_keys;
154 | crypto_class->get_signature_protocol = au_get_signature_protocol;
155 | crypto_class->get_encryption_protocol = au_get_encryption_protocol;
156 | crypto_class->get_key_exchange_protocol = au_get_key_exchange_protocol;
157 | }
158 |
159 | static void
160 | galore_au_context_init (GaloreAutoCryptContext *gpg, GaloreAutoCryptContextClass *klass)
161 | {
162 | gpg->ctx = NULL;
163 | }
164 |
165 | static void
166 | galore_au_context_finalize (GObject *object)
167 | {
168 | GaloreAutoCryptContext *gpg = (GaloreAutoCryptContext *) object;
169 |
170 | if (gpg->ctx)
171 | gpgme_release (gpg->ctx);
172 |
173 | G_OBJECT_CLASS (parent_class)->finalize (object);
174 | }
175 |
176 | static GMimeDigestAlgo
177 | au_digest_id (GMimeCryptoContext *ctx, const char *name)
178 | {
179 | if (name == NULL)
180 | return GMIME_DIGEST_ALGO_DEFAULT;
181 |
182 | if (!g_ascii_strncasecmp (name, "pgp-", 4))
183 | name += 4;
184 |
185 | if (!g_ascii_strcasecmp (name, "md2"))
186 | return GMIME_DIGEST_ALGO_MD2;
187 | else if (!g_ascii_strcasecmp (name, "md4"))
188 | return GMIME_DIGEST_ALGO_MD4;
189 | else if (!g_ascii_strcasecmp (name, "md5"))
190 | return GMIME_DIGEST_ALGO_MD5;
191 | else if (!g_ascii_strcasecmp (name, "sha1"))
192 | return GMIME_DIGEST_ALGO_SHA1;
193 | else if (!g_ascii_strcasecmp (name, "sha224"))
194 | return GMIME_DIGEST_ALGO_SHA224;
195 | else if (!g_ascii_strcasecmp (name, "sha256"))
196 | return GMIME_DIGEST_ALGO_SHA256;
197 | else if (!g_ascii_strcasecmp (name, "sha384"))
198 | return GMIME_DIGEST_ALGO_SHA384;
199 | else if (!g_ascii_strcasecmp (name, "sha512"))
200 | return GMIME_DIGEST_ALGO_SHA512;
201 | else if (!g_ascii_strcasecmp (name, "ripemd160"))
202 | return GMIME_DIGEST_ALGO_RIPEMD160;
203 | else if (!g_ascii_strcasecmp (name, "tiger192"))
204 | return GMIME_DIGEST_ALGO_TIGER192;
205 | else if (!g_ascii_strcasecmp (name, "haval-5-160"))
206 | return GMIME_DIGEST_ALGO_HAVAL5160;
207 |
208 | return GMIME_DIGEST_ALGO_DEFAULT;
209 | }
210 |
211 | static const char *
212 | au_digest_name (GMimeCryptoContext *ctx, GMimeDigestAlgo digest)
213 | {
214 | switch (digest) {
215 | case GMIME_DIGEST_ALGO_MD2:
216 | return "pgp-md2";
217 | case GMIME_DIGEST_ALGO_MD4:
218 | return "pgp-md4";
219 | case GMIME_DIGEST_ALGO_MD5:
220 | return "pgp-md5";
221 | case GMIME_DIGEST_ALGO_SHA1:
222 | return "pgp-sha1";
223 | case GMIME_DIGEST_ALGO_SHA224:
224 | return "pgp-sha224";
225 | case GMIME_DIGEST_ALGO_SHA256:
226 | return "pgp-sha256";
227 | case GMIME_DIGEST_ALGO_SHA384:
228 | return "pgp-sha384";
229 | case GMIME_DIGEST_ALGO_SHA512:
230 | return "pgp-sha512";
231 | case GMIME_DIGEST_ALGO_RIPEMD160:
232 | return "pgp-ripemd160";
233 | case GMIME_DIGEST_ALGO_TIGER192:
234 | return "pgp-tiger192";
235 | case GMIME_DIGEST_ALGO_HAVAL5160:
236 | return "pgp-haval-5-160";
237 | default:
238 | return "pgp-sha1";
239 | }
240 | }
241 |
242 | static const char *
243 | au_get_signature_protocol (GMimeCryptoContext *ctx)
244 | {
245 | return "application/pgp-signature";
246 | }
247 |
248 | static const char *
249 | au_get_encryption_protocol (GMimeCryptoContext *ctx)
250 | {
251 | return "application/pgp-encrypted";
252 | }
253 |
254 | static const char *
255 | au_get_key_exchange_protocol (GMimeCryptoContext *ctx)
256 | {
257 | return "application/pgp-keys";
258 | }
259 |
260 | static void
261 | set_passphrase_callback (GMimeCryptoContext *context)
262 | {
263 | GaloreAutoCryptContext *gpg = (GaloreAutoCryptContext *) context;
264 |
265 | if (context->request_passwd)
266 | gpgme_set_passphrase_cb (gpg->ctx, g_mime_gpgme_passphrase_callback, gpg);
267 | else
268 | gpgme_set_passphrase_cb (gpg->ctx, NULL, NULL);
269 | }
270 |
271 | static int
272 | au_sign (GMimeCryptoContext *context, gboolean detach, const char *userid,
273 | GMimeStream *istream, GMimeStream *ostream, GError **err)
274 | {
275 | gpgme_sig_mode_t mode = detach ? GPGME_SIG_MODE_DETACH : GPGME_SIG_MODE_CLEAR;
276 | GaloreAutoCryptContext *gpg = (GaloreAutoCryptContext *) context;
277 |
278 | set_passphrase_callback (context);
279 |
280 | gpgme_set_textmode (gpg->ctx, !detach);
281 |
282 | return g_mime_gpgme_sign (gpg->ctx, mode, userid, istream, ostream, err);
283 | }
284 |
285 | static GMimeSignatureList *
286 | au_verify (GMimeCryptoContext *context, GMimeVerifyFlags flags, GMimeStream *istream, GMimeStream *sigstream,
287 | GMimeStream *ostream, GError **err)
288 | {
289 | GaloreAutoCryptContext *gpg = (GaloreAutoCryptContext *) context;
290 |
291 | return g_mime_gpgme_verify (gpg->ctx, flags, istream, sigstream, ostream, err);
292 | }
293 |
294 | static int
295 | au_encrypt (GMimeCryptoContext *context, gboolean sign, const char *userid, GMimeEncryptFlags flags,
296 | GPtrArray *recipients, GMimeStream *istream, GMimeStream *ostream, GError **err)
297 | {
298 | GaloreAutoCryptContext *gpg = (GaloreAutoCryptContext *) context;
299 |
300 | if (sign)
301 | set_passphrase_callback (context);
302 |
303 | return g_mime_gpgme_encrypt (gpg->ctx, sign, userid, flags, recipients, istream, ostream, err);
304 | }
305 |
306 | static GMimeDecryptResult *
307 | au_decrypt (GMimeCryptoContext *context, GMimeDecryptFlags flags, const char *session_key,
308 | GMimeStream *istream, GMimeStream *ostream, GError **err)
309 | {
310 | GaloreAutoCryptContext *gpg = (GaloreAutoCryptContext *) context;
311 |
312 | set_passphrase_callback (context);
313 |
314 | return g_mime_gpgme_decrypt (gpg->ctx, flags, session_key, istream, ostream, err);
315 | }
316 |
317 | static int
318 | au_import_keys (GMimeCryptoContext *context, GMimeStream *istream, GError **err)
319 | {
320 | GaloreAutoCryptContext *gpg = (GaloreAutoCryptContext *) context;
321 |
322 | set_passphrase_callback (context);
323 |
324 | return g_mime_gpgme_import (gpg->ctx, istream, err);
325 | }
326 |
327 | static int
328 | au_export_keys (GMimeCryptoContext *context, const char *keys[], GMimeStream *ostream, GError **err)
329 | {
330 | GaloreAutoCryptContext *gpg = (GaloreAutoCryptContext *) context;
331 |
332 | set_passphrase_callback (context);
333 |
334 | // return g_mime_au_export (gpg->ctx, keys, ostream, err);
335 | return g_mime_gpgme_import (gpg->ctx, ostream, err);
336 | }
337 |
338 |
339 | /**
340 | * g_mime_gpg_context_new:
341 | *
342 | * Creates a new gpg crypto context object.
343 | *
344 | * Returns: (transfer full): a new gpg crypto context object.
345 | **/
346 | GMimeCryptoContext *
347 | galore_au_context_new (void)
348 | {
349 | GaloreAutoCryptContext *gpg;
350 | gpgme_ctx_t ctx;
351 |
352 | /* make sure GpgMe supports the OpenPGP protocols */
353 | if (gpgme_engine_check_version (GPGME_PROTOCOL_OpenPGP) != GPG_ERR_NO_ERROR)
354 | return NULL;
355 |
356 | /* create the GpgMe context */
357 | if (gpgme_new (&ctx) != GPG_ERR_NO_ERROR)
358 | return NULL;
359 | if (gpgme_ctx_set_engine_info(ctx, GPGME_PROTOCOL_OpenPGP, NULL, path) != GPG_ERR_NO_ERROR)
360 | return NULL;
361 |
362 | gpg = g_object_new (GALORE_TYPE_AU_CONTEXT, NULL);
363 | gpgme_set_protocol (ctx, GPGME_PROTOCOL_OpenPGP);
364 | gpgme_set_armor (ctx, TRUE);
365 | gpg->ctx = ctx;
366 |
367 | return (GMimeCryptoContext *) gpg;
368 | }
369 |
--------------------------------------------------------------------------------
/src/galore-autocrypt.h:
--------------------------------------------------------------------------------
1 | /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 | /* GMime
3 | * Copyright (C) 2000-2020 Jeffrey Stedfast
4 | *
5 | * This library is free software; you can redistribute it and/or
6 | * modify it under the terms of the GNU Lesser General Public License
7 | * as published by the Free Software Foundation; either version 2.1
8 | * of the License, or (at your option) any later version.
9 | *
10 | * This library is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 | * Lesser General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Lesser General Public
16 | * License along with this library; if not, write to the Free
17 | * Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
18 | * 02110-1301, USA.
19 | */
20 |
21 |
22 | #ifndef __GALORE_AU_CONTEXT_H__
23 | #define __GALORE_AU_CONTEXT_H__
24 |
25 | #include
26 |
27 | G_BEGIN_DECLS
28 |
29 | #define GALORE_TYPE_AU_CONTEXT (galore_au_context_get_type ())
30 | #define GALORE_AU_CONTEXT(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GALORE_TYPE_AU_CONTEXT, GMimeGpgContext))
31 | #define GALORE_AU_CONTEXT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GALORE_TYPE_AU_CONTEXT, GMimeGpgContextClass))
32 | #define GALORE_IS_AU_CONTEXT(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GALORE_TYPE_AU_CONTEXT))
33 | #define GALORE_IS_AU_CONTEXT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GALORE_TYPE_AU_CONTEXT))
34 | #define GALORE_AU_CONTEXT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GALORE_TYPE_AU_CONTEXT, GMimeGpgContextClass))
35 |
36 | typedef struct _GaloreAutoCryptContext GaloreAutoCryptContext;
37 | typedef struct _GaloreAutoCryptContextClass GaloreAutoCryptContextClass;
38 |
39 | GType galore_au_context_get_type (void);
40 |
41 | GMimeCryptoContext *galore_au_context_new (void);
42 |
43 | G_END_DECLS
44 |
45 | #endif /* __GALORE_AU_CONTEXT_H__ */
46 |
--------------------------------------------------------------------------------
/src/galore-filter-reply.c:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2009 Keith Packard
3 | *
4 | * This program is free software; you can redistribute it and/or modify
5 | * it under the terms of the GNU General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful, but
10 | * WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 | * General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU General Public License along
15 | * with this program; if not, write to the Free Software Foundation, Inc.,
16 | * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
17 | */
18 |
19 | #include
20 |
21 | #include "galore-filter-reply.h"
22 | // #include "notmuch-client.h"
23 |
24 | /**
25 | * SECTION: gmime-filter-reply
26 | * @title: GaloreFilterReply
27 | * @short_description: Add/remove reply markers
28 | *
29 | * A #GMimeFilter for adding or removing reply markers
30 | **/
31 | #define unused(x) x ## _unused __attribute__ ((unused))
32 |
33 | static void galore_filter_reply_class_init (GaloreFilterReplyClass *klass, void *class_data);
34 | static void galore_filter_reply_init (GaloreFilterReply *filter, GaloreFilterReplyClass *klass);
35 | static void galore_filter_reply_finalize (GObject *object);
36 |
37 | static GMimeFilter *filter_copy (GMimeFilter *filter);
38 | static void filter_filter (GMimeFilter *filter, char *in, size_t len, size_t prespace,
39 | char **out, size_t *outlen, size_t *outprespace);
40 | static void filter_complete (GMimeFilter *filter, char *in, size_t len, size_t prespace,
41 | char **out, size_t *outlen, size_t *outprespace);
42 | static void filter_reset (GMimeFilter *filter);
43 |
44 |
45 | static GMimeFilterClass *parent_class = NULL;
46 |
47 | GType
48 | galore_filter_reply_get_type (void)
49 | {
50 | static GType type = 0;
51 | if (!type) {
52 | static const GTypeInfo info = {
53 | .class_size = sizeof (GaloreFilterReplyClass),
54 | .base_init = NULL,
55 | .base_finalize = NULL,
56 | .class_init = (GClassInitFunc) galore_filter_reply_class_init,
57 | .class_finalize = NULL,
58 | .class_data = NULL,
59 | .instance_size = sizeof (GaloreFilterReply),
60 | .n_preallocs = 0,
61 | .instance_init = (GInstanceInitFunc) galore_filter_reply_init,
62 | .value_table = NULL,
63 | };
64 | type = g_type_register_static (GMIME_TYPE_FILTER, "GaloreFilterReply", &info, 0);
65 | }
66 | return type;
67 | }
68 |
69 | static void
70 | galore_filter_reply_class_init (GaloreFilterReplyClass *klass, unused (void *class_data))
71 | {
72 | GObjectClass *object_class = G_OBJECT_CLASS (klass);
73 | GMimeFilterClass *filter_class = GMIME_FILTER_CLASS (klass);
74 |
75 | parent_class = g_type_class_ref (GMIME_TYPE_FILTER);
76 | object_class->finalize = galore_filter_reply_finalize;
77 |
78 | filter_class->copy = filter_copy;
79 | filter_class->filter = filter_filter;
80 | filter_class->complete = filter_complete;
81 | filter_class->reset = filter_reset;
82 | }
83 |
84 | static void
85 | galore_filter_reply_init (GaloreFilterReply *filter, GaloreFilterReplyClass *klass)
86 | {
87 | (void) klass;
88 | filter->saw_nl = true;
89 | filter->saw_angle = false;
90 | }
91 |
92 | static void
93 | galore_filter_reply_finalize (GObject *object)
94 | {
95 | G_OBJECT_CLASS (parent_class)->finalize (object);
96 | }
97 |
98 |
99 | static GMimeFilter *
100 | filter_copy (GMimeFilter *filter)
101 | {
102 | GaloreFilterReply *reply = (GaloreFilterReply *) filter;
103 |
104 | return galore_filter_reply_new (reply->encode);
105 | }
106 |
107 | static void
108 | filter_filter (GMimeFilter *filter, char *inbuf, size_t inlen, size_t prespace,
109 | char **outbuf, size_t *outlen, size_t *outprespace)
110 | {
111 | GaloreFilterReply *reply = (GaloreFilterReply *) filter;
112 | const char *inptr = inbuf;
113 | const char *inend = inbuf + inlen;
114 | char *outptr;
115 |
116 | (void) prespace;
117 | if (reply->encode) {
118 | g_mime_filter_set_size (filter, 3 * inlen, false);
119 |
120 | outptr = filter->outbuf;
121 | while (inptr < inend) {
122 | if (reply->saw_nl) {
123 | *outptr++ = '>';
124 | // only add the space if there isn't a qoute
125 | if (*inptr != '>') {
126 | *outptr++ = ' ';
127 | }
128 | reply->saw_nl = false;
129 | }
130 | if (*inptr == '\n')
131 | reply->saw_nl = true;
132 | else
133 | reply->saw_nl = false;
134 | if (*inptr != '\r')
135 | *outptr++ = *inptr;
136 | inptr++;
137 | }
138 | } else {
139 | g_mime_filter_set_size (filter, inlen + 1, false);
140 |
141 | outptr = filter->outbuf;
142 | while (inptr < inend) {
143 | if (reply->saw_nl) {
144 | if (*inptr == '>')
145 | reply->saw_angle = true;
146 | else
147 | *outptr++ = *inptr;
148 | reply->saw_nl = false;
149 | } else if (reply->saw_angle) {
150 | if (*inptr == ' ')
151 | ;
152 | else
153 | *outptr++ = *inptr;
154 | reply->saw_angle = false;
155 | } else if (*inptr != '\r') {
156 | if (*inptr == '\n')
157 | reply->saw_nl = true;
158 | *outptr++ = *inptr;
159 | }
160 |
161 | inptr++;
162 | }
163 | }
164 |
165 | *outlen = outptr - filter->outbuf;
166 | *outprespace = filter->outpre;
167 | *outbuf = filter->outbuf;
168 | }
169 |
170 | static void
171 | filter_complete (GMimeFilter *filter, char *inbuf, size_t inlen, size_t prespace,
172 | char **outbuf, size_t *outlen, size_t *outprespace)
173 | {
174 | if (inbuf && inlen)
175 | filter_filter (filter, inbuf, inlen, prespace, outbuf, outlen, outprespace);
176 | }
177 |
178 | static void
179 | filter_reset (GMimeFilter *filter)
180 | {
181 | GaloreFilterReply *reply = (GaloreFilterReply *) filter;
182 |
183 | reply->saw_nl = true;
184 | reply->saw_angle = false;
185 | }
186 |
187 |
188 | /**
189 | * galore_filter_reply_new:
190 | * @encode: %true if the filter should encode or %false otherwise
191 | *
192 | * If @encode is %true, then all lines will be prefixed by "> ",
193 | * otherwise any lines starting with "> " will have that removed
194 | *
195 | * Returns: a new reply filter with @encode.
196 | **/
197 | GMimeFilter *
198 | galore_filter_reply_new (gboolean encode)
199 | {
200 | GaloreFilterReply *new_reply;
201 |
202 | new_reply = (GaloreFilterReply *) g_object_new (GALORE_TYPE_FILTER_REPLY, NULL);
203 | new_reply->encode = encode;
204 |
205 | return (GMimeFilter *) new_reply;
206 | }
207 |
--------------------------------------------------------------------------------
/src/galore-filter-reply.h:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright © 2009 Keith Packard
3 | *
4 | * This program is free software; you can redistribute it and/or modify
5 | * it under the terms of the GNU General Public License as published by
6 | * the Free Software Foundation, either version 3 of the License, or
7 | * (at your option) any later version.
8 | *
9 | * This program is distributed in the hope that it will be useful, but
10 | * WITHOUT ANY WARRANTY; without even the implied warranty of
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 | * General Public License for more details.
13 | *
14 | * You should have received a copy of the GNU General Public License along
15 | * with this program; if not, write to the Free Software Foundation, Inc.,
16 | * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
17 | */
18 |
19 | #ifndef _GALORE_FILTER_REPLY_H_
20 | #define _GALORE_FILTER_REPLY_H_
21 |
22 | #include
23 |
24 | void galore_filter_reply_module_init (void);
25 |
26 | G_BEGIN_DECLS
27 |
28 | #define GALORE_TYPE_FILTER_REPLY (galore_filter_reply_get_type ())
29 | #define GALORE_FILTER_REPLY(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), \
30 | GALORE_TYPE_FILTER_REPLY, \
31 | GaloreFilterReply))
32 | #define GALORE_FILTER_REPLY_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GALORE_TYPE_FILTER_REPLY, \
33 | GaloreFilterReplyClass))
34 | #define GALORE_IS_FILTER_REPLY(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), \
35 | GALORE_TYPE_FILTER_REPLY))
36 | #define GALORE_IS_FILTER_REPLY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), \
37 | GALORE_TYPE_FILTER_REPLY))
38 | #define GALORE_FILTER_REPLY_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GALORE_TYPE_FILTER_REPLY, \
39 | GaloreFilterReplyClass))
40 |
41 | typedef struct _GaloreFilterReply GaloreFilterReply;
42 | typedef struct _GaloreFilterReplyClass GaloreFilterReplyClass;
43 |
44 | /**
45 | * GaloreFilterReply:
46 | * @parent_object: parent #GMimeFilter
47 | * @encode: encoding vs decoding reply markers
48 | * @saw_nl: previous char was a \n
49 | * @saw_angle: previous char was a >
50 | *
51 | * A filter to insert/remove reply markers (lines beginning with >)
52 | **/
53 | struct _GaloreFilterReply {
54 | GMimeFilter parent_object;
55 |
56 | gboolean encode;
57 | gboolean saw_nl;
58 | gboolean saw_angle;
59 | };
60 |
61 | struct _GaloreFilterReplyClass {
62 | GMimeFilterClass parent_class;
63 |
64 | };
65 |
66 |
67 | GType galore_filter_reply_get_type (void);
68 |
69 | GMimeFilter *galore_filter_reply_new (gboolean encode);
70 |
71 | G_END_DECLS
72 |
73 |
74 | #endif /* _GALORE_FILTER_REPLY_H_ */
75 |
--------------------------------------------------------------------------------
/src/galore-gpgme-utils.h:
--------------------------------------------------------------------------------
1 | /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 | /* GMime
3 | * Copyright (C) 2000-2022 Jeffrey Stedfast
4 | *
5 | * This library is free software; you can redistribute it and/or
6 | * modify it under the terms of the GNU Lesser General Public License
7 | * as published by the Free Software Foundation; either version 2.1
8 | * of the License, or (at your option) any later version.
9 | *
10 | * This library is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 | * Lesser General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU Lesser General Public
16 | * License along with this library; if not, write to the Free
17 | * Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
18 | * 02110-1301, USA.
19 | */
20 |
21 |
22 | #ifndef __GMIME_GPGME_UTILS_H__
23 | #define __GMIME_GPGME_UTILS_H__
24 |
25 | #ifdef ENABLE_CRYPTO
26 |
27 | #include
28 | #include
29 | #include
30 |
31 | G_BEGIN_DECLS
32 |
33 | G_GNUC_INTERNAL gpgme_error_t g_mime_gpgme_passphrase_callback (void *hook, const char *uid_hint,
34 | const char *passphrase_info,
35 | int prev_was_bad, int fd);
36 |
37 | G_GNUC_INTERNAL int g_mime_gpgme_sign (gpgme_ctx_t ctx, gpgme_sig_mode_t mode, const char *userid,
38 | GMimeStream *istream, GMimeStream *ostream, GError **err);
39 |
40 | G_GNUC_INTERNAL GMimeSignatureList *g_mime_gpgme_verify (gpgme_ctx_t ctx, GMimeVerifyFlags flags, GMimeStream *istream,
41 | GMimeStream *sigstream, GMimeStream *ostream, GError **err);
42 |
43 | G_GNUC_INTERNAL int g_mime_gpgme_encrypt (gpgme_ctx_t ctx, gboolean sign, const char *userid,
44 | GMimeEncryptFlags flags, GPtrArray *recipients,
45 | GMimeStream *istream, GMimeStream *ostream,
46 | GError **err);
47 |
48 | G_GNUC_INTERNAL GMimeDecryptResult *g_mime_gpgme_decrypt (gpgme_ctx_t ctx, GMimeDecryptFlags flags, const char *session_key,
49 | GMimeStream *istream, GMimeStream *ostream, GError **err);
50 |
51 | G_GNUC_INTERNAL int g_mime_gpgme_import (gpgme_ctx_t ctx, GMimeStream *istream, GError **err);
52 |
53 | G_GNUC_INTERNAL int g_mime_gpgme_export (gpgme_ctx_t ctx, const char *keys[], GMimeStream *ostream, GError **err);
54 |
55 | G_END_DECLS
56 |
57 | #endif /* ENABLE_CRYPTO */
58 |
59 | #endif /* __GMIME_GPGME_UTILS_H__ */
60 |
--------------------------------------------------------------------------------
/src/galore.c:
--------------------------------------------------------------------------------
1 | #include "galore.h"
2 |
3 | // helper C functions for gmime
4 | void galore_init() {
5 | galore_filter_reply_get_type();
6 | }
7 |
--------------------------------------------------------------------------------
/src/galore.h:
--------------------------------------------------------------------------------
1 | #ifndef __GALORE_H__
2 | #define __GALORE_H__
3 |
4 | #include "galore-filter-reply.h"
5 |
6 | void galore_init();
7 |
8 | #endif /* __GALORE_H__ */
9 |
--------------------------------------------------------------------------------
/src/meson.build:
--------------------------------------------------------------------------------
1 | source_c = [
2 | 'galore.c',
3 | 'galore-filter-reply.c',
4 | ]
5 |
6 | source_h = [
7 | 'galore.h',
8 | 'galore-filter-reply.h',
9 | ]
10 |
11 | deps = [
12 | dependency('gmime-3.0'),
13 | dependency('glib-2.0'),
14 | dependency('gobject-2.0'),
15 | dependency('gio-2.0'),
16 | ]
17 | filter_lib = library('galore', source_c, dependencies : deps)
18 | gnome = import('gnome')
19 |
20 | if true
21 | gir_args = [
22 | '--quiet',
23 | '--warn-all',
24 | ]
25 |
26 | filter_glib_gir = gnome.generate_gir(
27 | filter_lib,
28 | sources: source_c + source_h,
29 | namespace: 'Galore',
30 | nsversion: '0.1',
31 | symbol_prefix: ['galore'],
32 | includes: [ 'GObject-2.0', 'Gio-2.0', 'GMime-3.0'],
33 | dependencies: deps,
34 | extra_args: gir_args,
35 | fatal_warnings: true,
36 | )
37 | else
38 | # Just warn and quit
39 | json_glib_gir = []
40 | endif
41 |
--------------------------------------------------------------------------------
/stylua.toml:
--------------------------------------------------------------------------------
1 | column_width = 100
2 | indent_type = "Spaces"
3 | indent_width = 2
4 | quote_style = "AutoPreferSingle"
5 |
--------------------------------------------------------------------------------
/syntax/galore-browser.vim:
--------------------------------------------------------------------------------
1 | setlocal conceallevel=3
2 | setlocal concealcursor=nciv
3 |
4 | " init should set this value
5 | " let GaloreFromLength = 10
6 | " let GaloreFromStr = "syntax match GaloreFrom '\s\+[^│]\{0,5}' contained nextgroup=GaloreFromConc"
7 |
8 | " Use this for now, I kinda want it to be order indpendent
9 | " Could I make the order generated and loaded on init?
10 | " conceal
11 | syntax region GaloreThreads start=/^/ end=/$/ oneline contains=GaloreDate
12 | " syntax match GaloreDate "\d\{4}-\d\{2}-\d\{2}" contained nextgroup=GaloreThreadCount
13 | syntax match GaloreDate "[0-9A-Za-z.\-]\+\(\s[a-z0-9:.]\+\)\?\(\sago\)\?" contained nextgroup=GaloreThreadCount
14 | syntax match GaloreThreadCount "\s\+\[[0-9]\+\/[0-9()]\+\]" contained nextgroup=GaloreFrom
15 | " seems to not work well with unicode, doesn't seem to be a unicode but a
16 | " space problem
17 | syntax match GaloreFrom '\s\+[^│]\{0,25}' contained nextgroup=GaloreFromConc
18 | " execute GaloreFromStr
19 | syntax match GaloreFromConc "[^│]*" contained nextgroup=GaloreFromEnd conceal
20 | syntax match GaloreFromEnd "│" contained nextgroup=GaloreSubject
21 | syntax match GaloreSubject /.\{0,}\(([^()]\+)$\)\@=/ contained nextgroup=GaloreTags
22 | syntax match GaloreTags "(.*)$" contained
23 |
24 | highlight GaloreFrom ctermfg=224 guifg=Orange
25 | highlight GaloreFromConc ctermfg=224 guifg=Green
26 | highlight GaloreFromEnd ctermfg=224 guifg=Red
27 | highlight link GaloreDate String
28 | highlight link GaloreThreadCount Comment
29 | highlight link GaloreSubject Statement
30 | highlight link GaloreTags Comment
31 |
--------------------------------------------------------------------------------
/syntax/galore-saved.vim:
--------------------------------------------------------------------------------
1 | syntax region GaloreSaved start=/^/ end=/$/ oneline contains=GaloreSavedCount
2 |
3 | syntax match GaloreSavedCount "\d*" contained nextgroup=GaloreSavedUnread
4 | syntax match GaloreSavedUnread "(\d*)" contained nextgroup=GaloreSavedName
5 | syntax match GaloreSavedName "\s\+.*\s" contained nextgroup=GaloreSavedSearch
6 | syntax match GaloreSavedSearch "([^()]\+)"
7 |
8 | highlight link GaloreSavedCount Statement
9 | highlight GaloreSavedUnread ctermfg=224 guifg=#9c453e
10 | highlight link GaloreSavedName Type
11 | highlight link GaloreSavedSearch String
12 |
13 | " highlight CursorLine term=reverse cterm=reverse gui=reverse
14 |
15 |
--------------------------------------------------------------------------------
/syntax/galore-threads.vim:
--------------------------------------------------------------------------------
1 | setlocal conceallevel=3
2 | setlocal concealcursor=nciv
3 |
4 | " init should set this value
5 | let GaloreFromLength = 10
6 | " let GaloreFromStr = "syntax match GaloreFrom '\s\+[^│]\{0,5}' contained nextgroup=GaloreFromConc"
7 |
8 | " Use this for now, I kinda want it to be order indpendent
9 | " Could I make the order generated and loaded on init?
10 | " conceal
11 | syntax region GaloreThreads start=/^/ end=/$/ oneline contains=GaloreDate
12 | syntax match GaloreDate "[0-9A-Za-z.\-]\+\(\s[a-z0-9:.]\+\)\?\(\sago\)\?" contained nextgroup=GaloreThreadCount
13 | syntax match GaloreThreadCount "\s\+\[[0-9]\+\/[0-9()]\+\]" contained nextgroup=GaloreFrom
14 | syntax match GaloreFrom '\s\+[^│]\{0,25}' contained nextgroup=GaloreFromConc
15 | " execute GaloreFromStr
16 | syntax match GaloreFromConc "[^│]*" contained nextgroup=GaloreFromEnd conceal
17 | syntax match GaloreFromEnd "│" contained nextgroup=GaloreSubject
18 | syntax match GaloreSubject /.\{0,}\(([^()]\+)$\)\@=/ contained nextgroup=GaloreTags
19 | syntax match GaloreTags "(.*)$" contained
20 |
21 | highlight GaloreFrom ctermfg=224 guifg=Orange
22 | highlight GaloreFromConc ctermfg=224 guifg=Green
23 | highlight GaloreFromEnd ctermfg=224 guifg=Red
24 | highlight link GaloreDate String
25 | highlight link GaloreThreadCount Comment
26 | highlight link GaloreSubject Statement
27 | highlight link GaloreTags Comment
28 |
--------------------------------------------------------------------------------
/test/ci_init.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # TEST_ROOT=$(dirname "$0")
4 | # git clone https://github.com/dagle/galore-test testdata
5 |
6 | MAIL_DIR="./testdata/testmail"
7 | mkdir -p ${HOME}/.config/notmuch/default
8 |
9 | cat <${HOME}/.config/notmuch/default/config
10 | [database]
11 | path=${MAIL_DIR}
12 |
13 | [user]
14 | name=Testi McTest
15 | primary_email=testi@testmail.org
16 | other_email=test_suite_other@testmailtwo.org;test_suite@otherdomain.org
17 | EOF
18 |
19 | # init
20 | notmuch new
21 |
22 | # run tests
23 |
24 | # fi
25 |
--------------------------------------------------------------------------------
/test/init.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ## setup test data
4 | TEST_ROOT=$(dirname "$0")
5 | TEST_ROOT=$(realpath ${TEST_ROOT})
6 | TMP_DIRECTORY="${TEST_ROOT}/testdir"
7 |
8 |
9 | if [[ ! -d "$TMP_DIRECTORY" ]]; then
10 | mkdir ${TMP_DIRECTORY}
11 | MAIL_DIR="${TMP_DIRECTORY}/testdata"
12 |
13 | if [[ ! -d "$MAIL_DIR" ]]; then
14 | git clone https://github.com/dagle/galore-test $MAIL_DIR
15 | fi
16 |
17 | # setup notmuch
18 | NOTMUCHDIR="${TMP_DIRECTORY}/notmuch/"
19 | mkdir ${NOTMUCHDIR}
20 | export NOTMUCH_CONFIG="${NOTMUCHDIR}/notmuch-config"
21 |
22 | cat <"${NOTMUCH_CONFIG}"
23 | [database]
24 | path=${MAIL_DIR}/testmail
25 | hook_dir=${NOTMUCHDIR}
26 |
27 | [user]
28 | name=Testi McTest
29 | primary_email=testi@testmail.org
30 | other_email=test_suite_other@testmailtwo.org;test_suite@otherdomain.org
31 | EOF
32 |
33 | # init
34 | export GNUPGHOME="${TMP_DIRECTORY}/gnupg"
35 | add_gnupg_home () {
36 | _gnupg_exit () { gpgconf --kill all 2>/dev/null || true; }
37 | at_exit_function _gnupg_exit
38 | mkdir -p -m 0700 "$GNUPGHOME"
39 | gpg --no-tty --import <$MAIL_DIR/testkey.asc >"$GNUPGHOME"/import.log 2>&1
40 |
41 | if (gpg --quick-random --version >/dev/null 2>&1) ; then
42 | echo quick-random >> "$GNUPGHOME"/gpg.conf
43 | elif (gpg --debug-quick-random --version >/dev/null 2>&1) ; then
44 | echo debug-quick-random >> "$GNUPGHOME"/gpg.conf
45 | fi
46 | echo no-emit-version >> "$GNUPGHOME"/gpg.conf
47 |
48 | FINGERPRINT="F998009F4AD9084F82096B36140FC9CB9DB2A71B"
49 | SELF_EMAIL="testi@testmail.org"
50 | printf '%s:6:\n' "$FINGERPRINT" | gpg --quiet --batch --no-tty --import-ownertrust
51 | gpgconf --kill all 2>/dev/null || true;
52 | }
53 |
54 | add_gnupg_home
55 |
56 | notmuch new
57 |
58 | fi
59 |
--------------------------------------------------------------------------------
/test/minimal.vim:
--------------------------------------------------------------------------------
1 | " Grabbing refactoring code
2 | set rtp+=.
3 |
4 | " Using local versions of plenary and nvim-treesitter if possible
5 | " This is required for CI
6 | set rtp+=../plenary.nvim
7 | set rtp+=../nvim-treesitter
8 | set rtp+=../telescope
9 | set rtp+=../filebrowser
10 | set rtp+=../notmuch-lua
11 |
12 | " If you use vim-plug if you got it locally
13 | set rtp+=~/.vim/plugged/plenary.nvim
14 | set rtp+=~/.vim/plugged/nvim-treesitter
15 | set rtp+=~/.vim/plugged/telescope.nvim
16 | set rtp+=~/.vim/plugged/telescope-file-browser.nvim
17 | set rtp+=~/.vim/plugged/notmuch-lua
18 |
19 | " If you are using packer
20 | set rtp+=~/.local/share/nvim/site/pack/packer/start/plenary.nvim
21 | set rtp+=~/.local/share/nvim/site/pack/packer/start/nvim-treesitter
22 | set rtp+=~/.local/share/nvim/site/pack/packer/start/telescope.nvim
23 | set rtp+=~/.local/share/nvim/site/pack/packer/start/telescope-file-browser.nvim
24 | set rtp+=~/.local/share/nvim/site/pack/packer/start/notmuch-lua
25 |
26 | runtime! plugin/plenary.vim
27 | runtime! plugin/nvim-treesitter.lua
28 | runtime! plugin/telescope.lua
29 |
30 | lua <