Skip to content

himalaya

himalaya is a Rust CLI to manage emails.

Features:

  • Multi-accounting
  • Interactive configuration via wizard (requires wizard feature)
  • Mailbox, envelope, message and flag management
  • Message composition based on $EDITOR
  • IMAP backend (requires imap feature)
  • Maildir backend (requires maildir feature)
  • Notmuch backend (requires notmuch feature)
  • SMTP backend (requires smtp feature)
  • Sendmail backend (requires sendmail feature)
  • Global system keyring for managing secrets (requires keyring feature)
  • OAuth 2.0 authorization (requires oauth2 feature)
  • JSON output via --output json
  • PGP encryption:
  • via shell commands (requires pgp-commands feature)
  • via GPG bindings (requires pgp-gpg feature)
  • via native implementation (requires pgp-native feature)

Cons:

  • Documentation is inexistent, you have to dive into the --help to understand stuff.

Installation

The v1.0.0 is currently being tested on the master branch, and is the prefered version to use. Previous versions (including GitHub beta releases and repositories published versions) are not recommended.

Himalaya CLI v1.0.0 can be installed with a pre-built binary. Find the latest pre-release GitHub workflow and look for the Artifacts section. You should find a pre-built binary matching your OS.

Himalaya CLI v1.0.0 can also be installed with cargo:

$ cargo install --git https://github.com/pimalaya/himalaya.git --force himalaya

Configuration

Just run himalaya, the wizard will help you to configure your default account.

You can also manually edit your own configuration, from scratch:

  • Copy the content of the documented ./config.sample.toml
  • Paste it in a new file ~/.config/himalaya/config.toml
  • Edit, then comment or uncomment the options you want

If using mbrsync

My generic configuration for an mbrsync account is:

[accounts.account_name]

email = "lyz@example.org"
display-name = "lyz"
envelope.list.table.unseen-char = "u"
envelope.list.table.replied-char = "r"
backend.type = "maildir"
backend.root-dir = "/home/lyz/.local/share/mail/lyz-example"
backend.maildirpp = false
message.send.backend.type = "smtp"
message.send.backend.host = "example.org"
message.send.backend.port = 587
message.send.backend.encryption = "start-tls"
message.send.backend.login = "lyz"
message.send.backend.auth.type = "password"
message.send.backend.auth.command = "pass show mail/lyz.example"

Once you've set it then you need to fix the INBOX directory.

Then you can check if it works by running himalaya envelopes list -a lyz-example

Vim plugin installation

Using lazy:

return {
  {
    "pimalaya/himalaya-vim",
  },
}

You can then run :Himalaya account_name and it will open himalaya in your editor.

Configure navigation bindings

The default bindings conflict with my git bindings, and to make them similar to orgmode agenda I'm changing the next and previous page:

return {
  {
    "pimalaya/himalaya-vim",
    keys = {
      { "b", "<plug>(himalaya-folder-select-previous-page)", desc = "Go to the previous email page" },
      { "f", "<plug>(himalaya-folder-select-next-page)", desc = "Go to the next email page" },
    },
  },
}

Configure the account bindings

To avoid typing :Himalaya account_name each time you want to check the email you can set some bindings:

return {
  {
    "pimalaya/himalaya-vim",
    keys = {
      { "<leader>ma", "<cmd>Himalaya account_name<cr>", desc = "Open account_name@example.org" },
      { "<leader>ml", "<cmd>Himalaya lyz<cr>", desc = "Open lyz@example.org" },
    },
  },
}

Setting the description is useful to see the configured accounts with which-key by typing <leader>m and waiting.

Configure extra bindings

The default plugin doesn't yet have all the bindings I'd like so I've added the next ones:

  • In the list of emails view:
  • dd in normal mode or d in visual: Delete emails
  • q: exit the program

  • In the email read view:

  • d: Delete email
  • q: Return to the list of emails view

  • In the email write view:

  • q or <c-s>: Save and send the email

If you want them too set the next config:

return {
  {
    "pimalaya/himalaya-vim",
    config = function()
      vim.api.nvim_create_augroup("HimalayaCustomBindings", { clear = true })
      vim.api.nvim_create_autocmd("FileType", {
        group = "HimalayaCustomBindings",
        pattern = "himalaya-email-listing",
        callback = function()
          -- Bindings to delete emails
          vim.api.nvim_buf_set_keymap(0, "n", "dd", "<plug>(himalaya-email-delete)", { noremap = true, silent = true })
          vim.api.nvim_buf_set_keymap(0, "x", "d", "<plug>(himalaya-email-delete)", { noremap = true, silent = true })
          -- Bind `q` to close the window
          vim.api.nvim_buf_set_keymap(0, "n", "q", ":bd<CR>", { noremap = true, silent = true })
        end,
      })

      vim.api.nvim_create_augroup("HimalayaEmailCustomBindings", { clear = true })
      vim.api.nvim_create_autocmd("FileType", {
        group = "HimalayaEmailCustomBindings",
        pattern = "mail",
        callback = function()
          -- Get the current buffer's full file path
          local filepath = vim.api.nvim_buf_get_name(0)

          -- Bind `q` and `<c-s>` to close the window
          if string.match(filepath, "write") then
            vim.api.nvim_buf_set_keymap(0, "n", "<c-s>", ":w<CR>:bd<CR>", { noremap = true, silent = true })
            vim.api.nvim_buf_set_keymap(0, "n", "q", ":w<CR>:bd<CR>", { noremap = true, silent = true })
          else
            -- In read mode you can't save the buffer
            vim.api.nvim_buf_set_keymap(0, "n", "q", ":bd<CR>", { noremap = true, silent = true })
          end

          -- Bind `d` to delete the email and close the window
          vim.api.nvim_buf_set_keymap(
            0,
            "n",
            "d",
            "<plug>(himalaya-email-delete):q<CR>",
            { noremap = true, silent = true }
          )
        end,
      })
    end,
  },
}

Configure email fetching from within vim

Fetching emails from within vim is not yet supported, so I'm manually refreshing by account:

return {
  {
    "pimalaya/himalaya-vim",
    keys = {
      -- Email refreshing bindings
      { "r", ":lua FetchEmails()<CR>", desc = "Fetch emails" },
    },
    config = function()
      function FetchEmails()
        local account = vim.api.nvim_eval("himalaya#domain#account#current()")

        vim.notify("Fetching emails for " .. account .. ", please wait...", vim.log.levels.INFO)
        vim.cmd("redraw")

        -- Start the mbsync job
        vim.fn.jobstart("mbsync " .. account, {
          on_exit = function(_, exit_code, _)
            if exit_code == 0 then
              vim.notify("Emails for " .. account .. " fetched successfully!", vim.log.levels.INFO)
              -- Reload Himalaya only after successful mbsync
              vim.cmd("Himalaya " .. account)
            else
              vim.notify("Failed to fetch emails for " .. account .. ". Check the logs.", vim.log.levels.ERROR)
            end
          end,
        })
      end
    end,
  },
}

Show notifications when emails arrive

You can set up mirador to get those notifications.

Not there yet

Troubleshooting

Emails are shown with different timezones

Set the account configuration envelope.list.datetime-local-tz = true

Emails are not being copied to Sent

Set the account configuration message.send.save-copy = true

Cannot find maildir matching name INBOX

mbrsync uses Inbox instead of the default INBOX so it doesn't find it. In theory you can use folder.alias.inbox = "Inbox" but it didn't work with me, so I finally ended up doing a symbolic link from INBOX to Inbox.

Cannot find maildir matching name Trash

That's because the Trash directory does not follow the Maildir structure. I had to create the cur tmp and new directories.

References