For those that like the UI emacs provides (or perhaps just enjoy the uniformity of using the same UI for programming and for everything else), emacs can be used to chat on slack, read email, and do various other internettey things.
Here are some of them:
- SSH Keys
- I keep my github ssh keys in lastpass, and access them with
M-x gds-load-github-keys
.
- I keep my github ssh keys in lastpass, and access them with
- Slack
- Trello
For details on how those things work, and what lastpass entries you need to create in order to make them work for you, see below.
A lot of the elisp in this file requires lexical binding. We should enable it up front, before getting into the meat of things.
;;; -*- lexical-binding: t -*-
A lot of the things I work on are stored in github repos. While emacs can manage local clones of those repos out-of-the-box, if I want to be able to push, I’ll need to load my creds.
To make this possible, I’ve already stored a private SSH key (with
a sensible passphrase) as a lastpass note in the location
Personal/github-ssh-key
.
This also makes use of our lastpass functions, and hence requires that you already have the lpass CLI installed.
To load your keys until 6pm (which, for me, is the end of the
working day), do M-x gds-load-github-keys
. If it’s after 6pm, or
you just want to load keys for a different amount of time, do M-x
gds-load-github-keys-for
and enter the number of hours of access
you want when prompted.
(defun gds-load-github-keys-for-seconds (seconds)
"Load my github keys from lastpass for SECONDS seconds.
Ensure I'm logged in to lastpass. Then load my github keys from
Personal/github-ssh-key for one SECONDS seconds."
(gds-lastpass-ensure-logged-in-and-then
(lambda ()
(let ((keyfile (make-temp-file "gdskeyfile")))
(with-temp-file keyfile)
(set-file-modes keyfile #o0600)
(with-temp-file keyfile
(insert (gds-lastpass-get-note "Personal/github-ssh-key")))
(when (get-process "ssh-add-github") ; Kill any previous stalled ssh-add attempt
(delete-process "ssh-add-github"))
(let ((process (start-process-shell-command
"ssh-add-github"
nil
(format "ssh-add -t %d %s" seconds keyfile))))
(set-process-filter
process
(lambda (proc string)
(when (string-match-p (regexp-quote "Enter passphrase for") string)
(process-send-string proc
(concat (read-passwd "Key passphrase? ") "\n")))
(when (string-match-p (regexp-quote "Identity added") string)
(delete-file keyfile)
(message "SSH Key successfully loaded")))))))))
(defun gds-load-github-keys-for (hours)
"Load my github keys from lastpass for HOURS hours.
Use `gds-load-github-keys-for-seconds' to load keys from
lastpass"
(interactive "nHow long for? ")
(gds-load-github-keys-for-seconds (* 3600 hours)))
(defun gds-load-github-keys ()
"Load my github keys from lastpass until 6pm.
Calculate how long it'll be until 6pm, then use
`gds-load-github-keys-for-seconds' to load my keys until then."
(interactive)
(let* ((now (current-time))
(now-decoded (decode-time now))
(eod (if (< emacs-major-version 27)
(encode-time 0 ;Seconds
0 ;Minutes
18 ;Hours
(nth 3 now-decoded) ;Day
(nth 4 now-decoded) ;Month
(nth 5 now-decoded)) ;Year
(encode-time (list
0 ;Seconds
0 ;Minutes
18 ;Hours
(nth 3 now-decoded) ;Day
(nth 4 now-decoded) ;Month
(nth 5 now-decoded) ;Year
(nth 6 now-decoded) ;DOW -- Ignored by encode but whatevs.
(nth 7 now-decoded) ;DST
(nth 8 now-decoded) ;Zone (UTC offset)
)))))
(if (time-less-p eod now)
(error "Looks like 6pm has been and gone")
(let ((timeleft (time-subtract eod now)))
(gds-load-github-keys-for-seconds (time-to-seconds timeleft))))))
If you want to use slack in emacs, this will do the trick. The code below downloads and configures the slack package by Yuya Minami. In order for this configuration to work, we need credentials. And of course, all credentials should be stored in lastpass.
Create a lastpass Note
at location Personal/slack-creds
, and put
the following JSON in there:
[{ "name": "my-slack-workspace",
"id": "aaaaaaaaaaa.00000000000",
"secret": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"token": "xoxs-sssssssssss-88888888888-hhhhhhhhhhh-jjjjjjjjjj"}]
Edit that json to connect to the slack workspace you’re interested in, and to use your own ID, secret, and token. Unfortunately you can’t use use OAuth or give it your regular password, so getting those credentials is a little involved. Read how here.
You might notice that the JSON you just created is a list of objects. If you want to configure multiple slack workspaces, create one object per workspace, and add them to your list.
Once your creds are in lastpass, you can configure and start
emacs-slack with M-x gds-slack-configure-and-start
, and the rest
should work as advertised.
(use-package slack
:ensure t
:commands (slack-start)
:init
(setq slack-buffer-emojify t)
(setq slack-prefer-current-team t))
(defun gds-slack-configure-and-start ()
"Configure slack from lastpass, and start it.
Assuming you have your slack creds in JSON format in lastpass,
this function will use those creds to configure your slack, and
will start it for you."
(interactive)
(gds-lastpass-ensure-logged-in-and-then
(lambda ()
(let* ((creds (json-read-from-string (gds-lastpass-get-note "Personal/slack-creds"))))
(map-do
(lambda (n team)
(slack-register-team
:name (alist-get 'name team)
:default t
:client-id (alist-get 'id team)
:client-secret (alist-get 'secret team)
:token (alist-get 'token team)
:subscribed-channels '()
:full-and-display-names t))
creds))
(slack-start))))
I think this would mean we get alerts for non-mentions in all channels in that list. I think the default is to get an alert for mentions, and for “open channels”.
It’s probably also worth checking out this keeping distractions under control blog before making decisions here.
If you don’t know why you would want to read email in emacs then no worries – it’s a pretty weird thing to do.
But if you keep getting frustrated at your email or webmail client because something that would be easy in your text editor is hard in an email, then maybe check out:
- the Mail Category in the emacs wiki for emacs solutions
- The following minimal mail clients that work with any text editor:
- mutt (See Alternative Email bits below)
- sup
- notmuch
If you like keeping your inbox empty (and probably using trello or org-mode as a todo list instead of using your inbox as a todo list), then this gnus setup might work for you.
Be warned, gnus is not a regular email client. It was originally a usenet news reader. When used to read email, I think it works best with a workflow in which every email is read at most once, and then either archived, deleted, or attached to some task so we can come back to it later.
To make this work, we’ll need some
credentials in lastpass. Create a lastpass Note
at location
Personal/email-creds
, and put the following JSON there:
{
"user-email-address": "[email protected]",
"user-full-name": "My Name",
"email-provider-name": "Gmail or whatever",
"imap-address": "imap.gmail.com or similar",
"smtp-address": "smtp.gmail.com or similar",
"smtp-port": "587 or similar",
"username": "my-username",
"password": "my-password",
"address-book-path": "~/my/address-book.ebdb"
}
If you’re using gmail, you’ll have to enable 2 factor auth, and create an app specific password. This is the password that you’ll need to put in your creds – not your regular google password.
Once your configuration is safely in lastpass, run M-x
gds-gnus-go
to read your mail.
If this is your first time using gnus, there are some decent introductions on the web, and once you have the rough idea, the gnus manual is very good.
If you’re using gmail, you can put your cursor over any mailbox
(including “All Mail”) and hit G G
to use gmail search on that
inbox.
We’re going to use IMAP to talk to gmail, because I want the view of my email on my phone to be synced with the view of my email on my laptop.
We’re going to use an elisp implementation of imap so that this configuration is as portable as possible. Unfortunately, that also makes it slow. And since emacs is single-threaded, when you hit “get mail”, it’ll lock up emacs for maybe 15 seconds while it syncs.
At some point, I might try to include an offlineimap setup in here or something like that.
There are millions of ways to do address books, and most of them exist at least once in the emacs ecosystem. For now, I’m going to try EBDB. In case this doesn’t work for you (or for future-me), here are some other options:
- Google contacts.el
- Org Contacts
- vdirel
- Good old BBDB
For now, I’m not going to worry about syncing with google contacts. I’m just going to make EBDB save its state to somewhere in a git repo.
(use-package ebdb
:ensure t)
I like to save the address of everyone who gets in touch with me.
(setq ebdb-mua-auto-update-p t)
We’ll tell EBDB where to save its state in the configuration below.
Most of the following configuration is borrowed straight from the emacswiki page for making gmail work with gnus. The authinfo shim is heavily informed by Daimian Cassou’s auth-password-store.
(defun gds-gnus-go ()
"Configure gnus from lastpass, then run it.
Use `gds-gnus-configure' to configure gnus using creds form
lastpass. Then run gnus."
(interactive)
(gds-gnus-configure-and-then
(lambda ()
(gnus))))
(defun gds-gnus-configure ()
"Pull email creds from lastpass and configure gnus with them."
(interactive)
(gds-gnus-configure-and-then (lambda ())))
(defun gds-gnus-configure-and-then (continuation)
"Asynchronously configure gnus with creds from lastpass.
Once we're done, call CONTINUATION."
(require 'nnir)
(gds-lastpass-ensure-logged-in-and-then
(lambda ()
(let* ((creds (json-read-from-string (gds-lastpass-get-note "Personal/email-creds")))
(mail-address (alist-get 'user-email-address creds))
(full-name (alist-get 'user-full-name creds))
(email-provider-name (alist-get 'email-provider-name creds))
(imap-address (alist-get 'imap-address creds))
(smtp-address (alist-get 'smtp-address creds))
(smtp-port (alist-get 'smtp-port creds))
(username (alist-get 'username creds))
(password (alist-get 'password creds))
(address-book-path (alist-get 'address-book-path creds)))
;; First configure gnus with non-secrets
(setq user-mail-address mail-address)
(setq user-full-name full-name)
(setq gnus-select-method
(list 'nnimap email-provider-name
(list 'nnimap-address imap-address)
'(nnimap-server-port "imaps")
'(nnimap-stream ssl)
'(nnir-search-engine imap)))
(add-to-list 'nnir-imap-search-arguments '("gmail" . "X-GM-RAW"))
(setq nnir-imap-default-search-key "gmail")
(setq smtpmail-smtp-server smtp-address
smtpmail-smtp-service smtp-port
gnus-ignored-newsgroups "^to\\.\\|^[0-9. ]+\\( \\|$\\)\\|^[\"]\"[#'()]")
(setq send-mail-function #'smtpmail-send-it)
;; Set the path to the ebdb address book
(setq ebdb-sources address-book-path)
(require 'ebdb-gnus)
(require 'ebdb-message)
;; Now shim our secrets into the auth-source framework, so we
;; don't have to manually type in our app-specific password.
(cl-defun gds-gnus-auth-source-search (&rest spec
&key backend type host user port
&allow-other-keys)
(let ((host-address (if (listp host)
(cadr host)
host)))
(cond ((string= imap-address host-address)
;; IMAP Gmail Creds
(list (list
:host host-address
:port "imaps"
:user username
:secret password)))
((string= smtp-address host-address)
;; SMTP Gmail Creds
(list (list
:host host-address
:port smtp-port
:user username
:secret password))))))
(defvar gds-gnus-auth-source-backend
(auth-source-backend "gds-gnus"
:source "." ;; not used
:type 'gds-gnus
:search-function #'gds-gnus-auth-source-search)
"Auth-source backend variable for gds-gnus shim.")
(add-to-list 'auth-sources 'gds-gnus)
(auth-source-forget-all-cached))
(defun gds-gnus-auth-source-backend-parse (entry)
"Create auth-source backend from ENTRY."
(when (eq entry 'gds-gnus)
(auth-source-backend-parse-parameters entry gds-gnus-auth-source-backend)))
;; Advice to add custom auth-source function
(if (boundp 'auth-source-backend-parser-functions)
(add-hook 'auth-source-backend-parser-functions #'gds-gnus-auth-source-backend-parse)
(advice-add 'auth-source-backend-parse :before-until #'gds-gnus-auth-source-backend-parse))
(message "Gnus configured from lastpass")
(funcall continuation))))
First, let’s disable the EBDB popup when it notices addresses. That thing gets annoying fast on a small laptop screen.
(setq ebdb-mua-pop-up nil)
This info page tells us how to customize how EBDB handles new email addresses in things like gnus. To begin with, I’d like to ignore any email address with “noreply” in it.
(defun gds-ebdb-check-gnus-addresses ()
"Check the From field of the current gnus article for 'noreply' addresses.
Intended to be called through `ebdb-mua-auto-update-p'. Return
nil if this email is from a noreply address, and t otherwise."
(save-excursion
(unless (string= "Message" mode-name)
(gnus-article-show-summary)
(let* ((msg-header (gnus-summary-article-header))
(from-line (mail-header-from msg-header)))
(not (or
(string-match-p "ask[a-zA-Z-]*@pivotal.io" from-line)
(string-match-p (regexp-quote "noreply") from-line)
(string-match-p (regexp-quote "no-reply") from-line)
(string-match-p (regexp-quote "do-not-reply") from-line)))))))
(setq ebdb-mua-auto-update-p #'gds-ebdb-check-gnus-addresses)
- State “TODO” from [2019-06-05 Wed 16:11]
fold.map
to test the lot.
Everything else in this section will work with any IMAP/SMTP email provider. However, there are two gmail-specific use-cases I find handy:
- I’m reading email in gnus, and want to create a filter so that this sort of email always arrives in a different inbox.
- I capture a task while reading an email in gnus, and that task gets synced to trello. Later, I’m reading that task in trello, and want to follow a link back to the email that inspired the task.
Both of these use-cases need a way of getting a gmail URI from the current email.
(defun gds-gnus-get-gmail-link ()
"Get a gmail link for the current article.
Extract the message-id from the article header, then construct a
gmail URL to find it"
(let* ((msg-header (gnus-summary-article-header))
(msg-id (mail-header-id msg-header))
(url-id (url-hexify-string (format "rfc822msgid:%s" msg-id))))
(format "https://mail.google.com/mail/u/0/?ibxr=0#search/%s" url-id)))
Given a hyperlink to a given email in gmail, it’s easy enough to open that link in our default browser.
(defun gds-gnus-open-in-gmail ()
"Open the current article in gmail."
(interactive)
(browse-url (gds-gnus-get-gmail-link)))
Given a hyperlink to a given email in gmail, we can wrap
org-capture
in a function which saves that link to a buffer,
which can then be used to construct a task that will make sense
even from the trello view.
(defun gds-gnus-org-capture (&optional goto keys)
"Wrap org-capture, and store a gmail link.
Call `org-capture', but first store a link to this email in
Gmail. This can be used later by `gds-org-pop-gmail-link' to
construct a task that's useful outside emacs."
(interactive "P")
(setq gds-org-gmail-link-buffer (gds-gnus-get-gmail-link))
(org-capture goto keys))
(define-minor-mode gds-gnus-summary-mode
"Toggle gds-gnus-summary-mode
Gds-gnus-summary-mode adds gds's extra keyboard shortcuts to
gnus. Right now, that just means a function to open the current
message in gmail will be bound to C-c g."
:keymap (let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c g") #'gds-gnus-open-in-gmail)
(define-key map (kbd "C-c c") #'gds-gnus-org-capture)
map))
(add-hook 'gnus-summary-mode-hook #'gds-gnus-summary-mode)
Sometimes email in plain text is way too boring. Fortunately, org-mode documents can be exported into all the formats under the sun, including multipart mime.
The following is the recommended config:
(use-package org-mime
:ensure t
:delight)
(add-hook 'message-mode-hook
(lambda ()
(local-set-key "\C-c\M-o" 'org-mime-htmlize)))
(add-hook 'org-mode-hook
(lambda ()
(local-set-key "\C-c\M-o" 'org-mime-org-buffer-htmlize)))
All that stuff is pretty ugly in-line, and would be testable as a library.
Sometimes it’s nice to be able to use emacs with external MUAs, such as mutt. To do this comfortably we’ll need:
Hooks to put mutt-emails in an email composition mode:
(add-to-list 'auto-mode-alist '("/mutt" . mail-mode))
This will work fine on its own, but on a US keyboard layout,
pressing C-c C-#
to exit the editor is a pain in the left
hand. Far better to use the standard emacs “do what I mean” command
C-c C-c
:
(add-hook
'mail-mode-hook
(lambda ()
(define-key mail-mode-map (kbd "C-c C-c")
(lambda ()
"Save and exit the client"
(interactive)
(save-buffer)
(server-edit)))))
And, a mode for editing mutt config files:
(use-package mutt-mode
:ensure t
:commands (mutt-mode))
If we’re on a system with snap
(such as ubuntu), then we can use snap to install
maildir-utils
:
sudo snap install maildir-utils
Then we can load mu4e
from the snap:
(if (file-exists-p "/snap/maildir-utils/current/share/emacs/site-lisp/mu4e")
(progn
(add-to-list 'load-path "/snap/maildir-utils/current/share/emacs/site-lisp/mu4e")
(require 'mu4e)))
If we have the maildir-utils
snap, then the info page we want is
in /snap/maildir-utils/current/share/info
. To access this info
page, we need to add it to our info index. There are various
options and steps and you can read about them in detail here.
For our case, we’ll start by adding the snap info directory to our list of info directories:
(add-to-list 'Info-directory-list "/snap/maildir-utils/current/share/info")
Now we should be able to follow org links like this one to the
info file. However, we won’t see Mu4e in the info directory if we
just hit C-h i
. For that, we need a dir
file that contains an
entry for Mu4e, and we need that dir file also be in a path in
Info-directory-list
.
Annoyingly, there’s no dir
file provided in the snap, so we’ll
have to create one:
install-info /snap/maildir-utils/current/share/info/mu4e.info ~/.emacs.d/info/dir
I’ve checked that dir file into this repo, so you don’t have to run the same command.
Now that we have a dir file that points to the right place, we can
add it to Info-directory-list
:
(add-to-list 'Info-directory-list "~/.emacs.d/info")
Now we should have access to the Mu4e docs from within emacs. If
you want to access them from the CLI too, you’ll need to add the
following lines to your .bashrc
or similar:
export INFOPATH="$INFOPATH:/snap/maildir-utils/current/share/info"
export INFOPATH="$INFOPATH:$HOME/.emacs.d/info"
A lot of executive emails in outlook seem to be full of tables, which get displayed in mu4e as really long space-padded lines. This is annoying, and breaks up our paragraphs.
This function takes a read-only message window, and removes the blank spaces at the end of each line.
(defun gds-clean-exec-email ()
(interactive)
(beginning-of-buffer)
(read-only-mode 0)
(replace-regexp " *$" "")
(read-only-mode 1)
(beginning-of-buffer))
When viewing html email, we can make tab
and backtab
keys
cycle through hyperlinks.
(add-hook 'mu4e-view-mode-hook
(lambda()
;; try to emulate some of the eww key-bindings
(local-set-key (kbd "<tab>") 'shr-next-link)
(local-set-key (kbd "<backtab>") 'shr-previous-link)))
And I like to use a dark theme, so it’s worth setting a max luminosity for me.
(setq shr-color-visible-luminance-min 80)
Mu4e needs you to keep your email in a local maildir folder, so I’m not going to try to build a generic config like I did for gnus and lastpass above. Instead, let’s load local email config from another repo if it exists.
(if (file-exists-p "~/.mu4e-config.org")
(org-babel-load-file "~/.mu4e-config.org"))
To figure out what to put in there, I refer you to the excellent mu4e info page that you should now have access to.
I’d like to be able to use org-mode for tracking my todo lists while I’m on a laptop, but also be able to access them on my phone, and collaborate with other people on them.
Again, we’ll need to store some credentials in lastpass, and those
credentials won’t be a standard username and password. They’ll be
Oauth tokens. In this case org-trello
offers us some helper
functions to get them. Do M-x org-trello-install-key-and-token
and follow the instructions here.
This will result in a file appearing in
~/.emacs.d/.trello/$TRELLO_USERNAME.el
. That file will contain
the credentials you need to connect to trello, so we won’t want to
leave it lying around forever. We’ll copy those creds into
lastpass, then delete that file.
In lastpass, we’ll need our creds, and also the list of files that
we intend to use with trello. Save this note in
Personal/trello-creds
.
{
"key": "the org-trello-consumer-key",
"token": "the org-trello-access-token",
"files": [
"~/path/to/todo.org"
]
}
Once your creds are in lastpass, it’s safe to delete
~/.emacs.d/.trello/$TRELLO_USERNAME.el
.
Notice that for now we only support a single trello account per lastpass account. Org-trello can support multiple trello accounts, which is why its config files are namespaced by user.
I’d rather not have credentials on my filesystem, so there’s a shim below that inserts our credentials from lastpass where org-trello would usually load them from disk. For now, this means limiting us to one trello account per lastpass account.
(use-package org-trello
:ensure t)
(defun gds-trello-setup ()
"Configure org-trello using creds from lastpass."
(interactive)
(gds-lastpass-ensure-logged-in-and-then
(lambda ()
(let* ((creds-json (gds-lastpass-get-note "Personal/trello-creds"))
(creds (json-read-from-string creds-json))
(key (alist-get 'key creds))
(token (alist-get 'token creds))
(files (alist-get 'files creds)))
(setq org-trello-files (map 'list #'identity files))
(mapc (lambda (file)
(setq org-default-notes-file file)
(cl-pushnew file org-agenda-files))
org-trello-files)
;; Shim our creds into all the interesting files, so org-trello
;; never tries to load the config file we deleted.
(add-hook 'org-mode-hook
(lambda ()
(setq org-trello-consumer-key key
org-trello-access-token token)))
(defun gds-orgtrello-load-keys-shim (&rest args)
"We already loaded keys from lastpass, so just return :ok"
:ok)
(advice-add 'orgtrello-controller-load-keys
:override
#'gds-orgtrello-load-keys-shim)
(message "Trello Successfully Configured")))))
I keep my main todo list in a single org-mode file, and sync it with trello using the kit above. This means I often want to:
- Sync my org file to trello
- Save the resulting metadata to the org file
- Git commit the org file with message “todo”
- Git push
Here’s a little thing for helping with that. For now my workflow
is C-c o s
to sync to trello, followed by C-c o p
to save,
commit, and push to git.
(defun gds-todo-commit-and-push ()
"Commit and push the current file with message \"todo\"."
(interactive)
(save-buffer)
(magit-stage-file (buffer-file-name))
(magit-commit (list "-m" "todo"))
(magit-push-current-to-pushremote nil))
(add-hook 'org-trello-mode-hook
(lambda ()
(local-set-key (kbd "C-c o p") 'gds-todo-commit-and-push)))
Of course in order to do anything on the internet, we’ll need credentials. And storing those in a config file in git would be unwise. Let’s keep them in lastpass!
Unhappily, as I write this, the lastpass module on melpa hasn’t been updated in over a year, and doesn’t seem to work with the latest CLI. So we’ll have to roll our own for the tiny things we want.
The bare minimum is to be able to log in and out. Let’s assume that
the lpass
CLI is in the $PATH
.
(defun gds-lastpass-ensure-logged-in-and-then (continuation)
"Ensure the lpass CLI is logged in, then call CONTINUATION.
Check with `lpass status` if we're logged in. If not, log
in. Once we're logged in, call CONTINUATION."
(let ((lpass-status (shell-command-to-string "lpass status --color=never")))
(when (string-match-p (regexp-quote "Logged in as") lpass-status)
(message "Lastpass was already logged in")
(funcall continuation))
(when (string-match-p (regexp-quote "Not logged in.") lpass-status)
(when (get-process "lastpass") ; Kill any previous stalled login attempt
(delete-process "lastpass"))
;; Start trying to log in
(let* ((username (read-string "Who should we log in to lastpass as? "))
(process (start-process-shell-command
"lastpass"
nil
(concat "LPASS_DISABLE_PINENTRY=1 lpass login "
(shell-quote-argument username)))))
(set-process-filter
process
;; Respond to password and 2fa challenges
(lambda (proc string)
(when (string-match-p (regexp-quote "Master Password") string)
(process-send-string proc
(concat (read-passwd "Lastpass Master Password? ") "\n")))
(when (string-match-p (regexp-quote "Code") string)
(unless (string-match-p (regexp-quote "out-of-band") string)
(process-send-string proc
(concat (read-passwd "2FA Code? ") "\n"))))
(when (string-match-p (regexp-quote "Success") string)
(message "Lastpass logged in")
(funcall continuation)))))))) ; This needs lexical binding
(defun gds-lastpass-login ()
"Ensure the lpass CLI is logged in.
Check with `lpass status` if we're logged in. If not, log in."
(interactive)
(gds-lastpass-ensure-logged-in-and-then (lambda ())))
(defun gds-lastpass-logout ()
"Ensure the lpass CLI is logged out."
(interactive)
(shell-command "lpass logout -f")
(message "Lastpass logged out"))
(defun gds-lastpass-get-note (note-path)
"Get a secure note from lastpass.
We must already be logged in to lastpass for this to work. Use
`gds-lastpass-ensure-logged-in-and-then' to be sure."
(shell-command-to-string
(format "lpass show %s --notes" note-path)))