mournmail 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/README.md +46 -27
- data/lib/mournmail.rb +2 -0
- data/lib/mournmail/commands.rb +7 -4
- data/lib/mournmail/config.rb +2 -4
- data/lib/mournmail/draft_mode.rb +56 -13
- data/lib/mournmail/mail_encoded_word_patch.rb +73 -0
- data/lib/mournmail/message_mode.rb +3 -2
- data/lib/mournmail/message_rendering.rb +22 -15
- data/lib/mournmail/search_result_mode.rb +144 -0
- data/lib/mournmail/summary.rb +63 -8
- data/lib/mournmail/summary_mode.rb +406 -38
- data/lib/mournmail/utils.rb +279 -38
- data/lib/mournmail/version.rb +1 -1
- data/mournmail.gemspec +1 -1
- metadata +6 -5
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 | 
            -
             | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 2 | 
            +
            SHA256:
         | 
| 3 | 
            +
              metadata.gz: 422feadab2e36499c76f69f1836ce4e384497f0e86814cb4c20d0966e70b69b8
         | 
| 4 | 
            +
              data.tar.gz: 82423aa120aef42601ef86737e8461b96c32f786d9e7e647c80a4dfdc14a93f1
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 7a4bba313e307593fad8635b6f284bbc58a79087adad0671849bbf3ec416e554ba7c4af258f4b6556606262897035a08313d5f40754cde56bf8652555049ef09
         | 
| 7 | 
            +
              data.tar.gz: 6a30de329d0dc20d86954c1cb8d881bfe15b8309931f2fbba98cdd1a690aa53d6eab1f345b86822c2a6de19905e357232fea1e2092077d866b1f9c4fc2a630d1
         | 
    
        data/README.md
    CHANGED
    
    | @@ -10,34 +10,53 @@ Mournmail is a message user agent for | |
| 10 10 | 
             
            ## Configuration
         | 
| 11 11 |  | 
| 12 12 | 
             
            ```ruby
         | 
| 13 | 
            -
             | 
| 14 | 
            -
             | 
| 15 | 
            -
             | 
| 16 | 
            -
             | 
| 17 | 
            -
             | 
| 18 | 
            -
             | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 21 | 
            -
             | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 27 | 
            -
             | 
| 28 | 
            -
             | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 31 | 
            -
             | 
| 32 | 
            -
             | 
| 33 | 
            -
             | 
| 34 | 
            -
             | 
| 35 | 
            -
             | 
| 36 | 
            -
                : | 
| 13 | 
            +
            CONFIG[:mournmail_accounts] = {
         | 
| 14 | 
            +
              "example.com" => {
         | 
| 15 | 
            +
                from: "Shugo Maeda <shugo@example.com>",
         | 
| 16 | 
            +
                delivery_method: :smtp,
         | 
| 17 | 
            +
                delivery_options: {
         | 
| 18 | 
            +
                  address: "smtp.example.com",
         | 
| 19 | 
            +
                  port: 465,
         | 
| 20 | 
            +
                  domain: Socket.gethostname,
         | 
| 21 | 
            +
                  user_name: "shugo",
         | 
| 22 | 
            +
                  password: File.read("/path/to/smtp_passwd").chomp,
         | 
| 23 | 
            +
                  authentication: "login",
         | 
| 24 | 
            +
                  tls: true,
         | 
| 25 | 
            +
                  ca_file: "/path/to/ca.pem"
         | 
| 26 | 
            +
                },
         | 
| 27 | 
            +
                imap_host: "imap.example.com",
         | 
| 28 | 
            +
                imap_options: {
         | 
| 29 | 
            +
                  auth_type: "PLAIN",
         | 
| 30 | 
            +
                  user_name: "shugo",
         | 
| 31 | 
            +
                  password: File.read("/path/to/imap_passwd").chomp,
         | 
| 32 | 
            +
                  ssl: { ca_file: "/path/to/ca.pem" }
         | 
| 33 | 
            +
                },
         | 
| 34 | 
            +
                spam_mailbox: "spam",
         | 
| 35 | 
            +
                outbox_mailbox: "outbox",
         | 
| 36 | 
            +
                archive_mailbox_format: "archive/%Y"
         | 
| 37 | 
            +
              },
         | 
| 38 | 
            +
              "gmail.com" => {
         | 
| 39 | 
            +
                from: "Example <example@gmail.com>",
         | 
| 40 | 
            +
                delivery_method: :smtp,
         | 
| 41 | 
            +
                delivery_options: {
         | 
| 42 | 
            +
                  address: "smtp.gmail.com",
         | 
| 43 | 
            +
                  port: 587,
         | 
| 44 | 
            +
                  domain: Socket.gethostname,
         | 
| 45 | 
            +
                  user_name: "example@gmail.com",
         | 
| 46 | 
            +
                  password: File.read("/path/to/gmail_passwd").chomp,
         | 
| 47 | 
            +
                  authentication: "login",
         | 
| 48 | 
            +
                  enable_starttls_auto: true
         | 
| 49 | 
            +
                },
         | 
| 50 | 
            +
                imap_host: "imap.gmail.com",
         | 
| 51 | 
            +
                imap_options: {
         | 
| 52 | 
            +
                  auth_type: "PLAIN",
         | 
| 53 | 
            +
                  user_name: "example@gmail.com",
         | 
| 54 | 
            +
                  password: File.read(File.expand_path("~/.textbringer/gmail_passwd")).chomp,
         | 
| 55 | 
            +
                  ssl: true
         | 
| 56 | 
            +
                },
         | 
| 57 | 
            +
                spam_mailbox: "迷惑メール",
         | 
| 58 | 
            +
                archive_mailbox_format: false
         | 
| 37 59 | 
             
              },
         | 
| 38 | 
            -
              auth_type: "PLAIN",
         | 
| 39 | 
            -
              user_name: "shugo",
         | 
| 40 | 
            -
              password: File.read("/path/to/imap_passwd").chomp
         | 
| 41 60 | 
             
            }
         | 
| 42 61 | 
             
            ```
         | 
| 43 62 |  | 
    
        data/lib/mournmail.rb
    CHANGED
    
    | @@ -9,4 +9,6 @@ require_relative "mournmail/summary" | |
| 9 9 | 
             
            require_relative "mournmail/draft_mode"
         | 
| 10 10 | 
             
            require_relative "mournmail/summary_mode"
         | 
| 11 11 | 
             
            require_relative "mournmail/message_mode"
         | 
| 12 | 
            +
            require_relative "mournmail/search_result_mode"
         | 
| 12 13 | 
             
            require_relative "mournmail/commands"
         | 
| 14 | 
            +
            require_relative "mournmail/mail_encoded_word_patch"
         | 
    
        data/lib/mournmail/commands.rb
    CHANGED
    
    | @@ -1,11 +1,12 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 3 | 
             
            define_command(:mournmail, doc: "Start mournmail.") do
         | 
| 4 | 
            +
              Mournmail.open_groonga_db
         | 
| 4 5 | 
             
              mournmail_visit_mailbox("INBOX")
         | 
| 5 6 | 
             
            end
         | 
| 6 7 |  | 
| 7 8 | 
             
            define_command(:mournmail_visit_mailbox, doc: "Start mournmail.") do
         | 
| 8 | 
            -
              |mailbox =  | 
| 9 | 
            +
              |mailbox = Mournmail.read_mailbox_name("Visit mailbox: ", default: "INBOX")|
         | 
| 9 10 | 
             
              mournmail_summary_sync(mailbox)
         | 
| 10 11 | 
             
            end
         | 
| 11 12 |  | 
| @@ -33,10 +34,10 @@ define_command(:mournmail_summary_sync, doc: "Sync summary.") do | |
| 33 34 | 
             
                  message("Syncing #{mailbox} in background... Done")
         | 
| 34 35 | 
             
                  begin
         | 
| 35 36 | 
             
                    buffer.beginning_of_buffer
         | 
| 36 | 
            -
                    buffer.re_search_forward( | 
| 37 | 
            +
                    buffer.re_search_forward(/^ *\d+ u/)
         | 
| 37 38 | 
             
                  rescue SearchError
         | 
| 38 39 | 
             
                    buffer.end_of_buffer
         | 
| 39 | 
            -
                    buffer.re_search_backward( | 
| 40 | 
            +
                    buffer.re_search_backward(/^ *\d+ /, raise_error: false)
         | 
| 40 41 | 
             
                  end
         | 
| 41 42 | 
             
                  summary_read_command
         | 
| 42 43 | 
             
                end
         | 
| @@ -63,6 +64,7 @@ define_command(:mournmail_quit, doc: "Quit mournmail.") do | |
| 63 64 | 
             
              Mournmail.current_summary = nil
         | 
| 64 65 | 
             
              Mournmail.current_mail = nil
         | 
| 65 66 | 
             
              Mournmail.current_uid = nil
         | 
| 67 | 
            +
              Mournmail.close_groonga_db
         | 
| 66 68 | 
             
            end
         | 
| 67 69 |  | 
| 68 70 | 
             
            define_command(:mail, doc: "Write a new mail.") do
         | 
| @@ -70,8 +72,9 @@ define_command(:mail, doc: "Write a new mail.") do | |
| 70 72 | 
             
              buffer = Buffer.new_buffer("*draft*")
         | 
| 71 73 | 
             
              switch_to_buffer(buffer)
         | 
| 72 74 | 
             
              draft_mode
         | 
| 75 | 
            +
              conf = Mournmail.account_config
         | 
| 73 76 | 
             
              insert <<~EOF
         | 
| 74 | 
            -
                From: #{ | 
| 77 | 
            +
                From: #{conf[:from]}
         | 
| 75 78 | 
             
                To: 
         | 
| 76 79 | 
             
                Subject: 
         | 
| 77 80 | 
             
                User-Agent: Mournmail/#{Mournmail::VERSION} Textbringer/#{Textbringer::VERSION} Ruby/#{RUBY_VERSION}
         | 
    
        data/lib/mournmail/config.rb
    CHANGED
    
    | @@ -2,9 +2,7 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            module Textbringer
         | 
| 4 4 | 
             
              CONFIG[:mournmail_directory] = File.expand_path("~/.mournmail")
         | 
| 5 | 
            -
              CONFIG[: | 
| 6 | 
            -
              CONFIG[:mournmail_delivery_method] = :smtp
         | 
| 7 | 
            -
              CONFIG[:mournmail_delivery_options] = {}
         | 
| 5 | 
            +
              CONFIG[:mournmail_accounts] = {}
         | 
| 8 6 | 
             
              CONFIG[:mournmail_charset] = "utf-8"
         | 
| 9 7 | 
             
              CONFIG[:mournmail_save_directory] = "/tmp"
         | 
| 10 8 | 
             
              CONFIG[:mournmail_display_header_fields] = [
         | 
| @@ -19,8 +17,8 @@ module Textbringer | |
| 19 17 | 
             
                "Content-Type"
         | 
| 20 18 | 
             
              ]
         | 
| 21 19 | 
             
              CONFIG[:mournmail_imap_connect_timeout] = 10
         | 
| 20 | 
            +
              CONFIG[:mournmail_keep_alive_interval] = 60
         | 
| 22 21 | 
             
              CONFIG[:mournmail_file_open_comamnd] = "xdg-open"
         | 
| 23 22 | 
             
              CONFIG[:mournmail_link_open_comamnd] = "xdg-open"
         | 
| 24 | 
            -
              CONFIG[:mournmail_outbox] = nil
         | 
| 25 23 | 
             
              CONFIG[:mournmail_addresses_path] = File.expand_path("~/.addresses")
         | 
| 26 24 | 
             
            end
         | 
    
        data/lib/mournmail/draft_mode.rb
    CHANGED
    
    | @@ -7,6 +7,8 @@ module Mournmail | |
| 7 7 | 
             
                MAIL_MODE_MAP.define_key("\C-c\C-k", :draft_kill_command)
         | 
| 8 8 | 
             
                MAIL_MODE_MAP.define_key("\C-c\C-x\C-i", :draft_attach_file_command)
         | 
| 9 9 | 
             
                MAIL_MODE_MAP.define_key("\t", :draft_complete_or_insert_tab_command)
         | 
| 10 | 
            +
                MAIL_MODE_MAP.define_key("\C-c\C-xv", :draft_pgp_sign_command)
         | 
| 11 | 
            +
                MAIL_MODE_MAP.define_key("\C-c\C-xe", :draft_pgp_encrypt_command)
         | 
| 10 12 |  | 
| 11 13 | 
             
                define_syntax :field_name, /^[A-Za-z\-]+: /
         | 
| 12 14 | 
             
                define_syntax :quotation, /^>.*/
         | 
| @@ -22,6 +24,7 @@ module Mournmail | |
| 22 24 | 
             
                  unless y_or_n?("Send this mail?")
         | 
| 23 25 | 
             
                    return
         | 
| 24 26 | 
             
                  end
         | 
| 27 | 
            +
                  run_hooks(:mournmail_pre_send_hook)
         | 
| 25 28 | 
             
                  s = @buffer.to_s
         | 
| 26 29 | 
             
                  charset = CONFIG[:mournmail_charset]
         | 
| 27 30 | 
             
                  begin
         | 
| @@ -30,15 +33,22 @@ module Mournmail | |
| 30 33 | 
             
                    charset = "utf-8"
         | 
| 31 34 | 
             
                  end
         | 
| 32 35 | 
             
                  m = Mail.new(charset: charset)
         | 
| 36 | 
            +
                  m.transport_encoding = "8bit"
         | 
| 33 37 | 
             
                  header, body = s.split(/^--text follows this line--\n/, 2)
         | 
| 34 38 | 
             
                  attached_files = []
         | 
| 35 39 | 
             
                  attached_messages = []
         | 
| 40 | 
            +
                  pgp_sign = false
         | 
| 41 | 
            +
                  pgp_encrypt = false
         | 
| 36 42 | 
             
                  header.scan(/^([!-9;-~]+):[ \t]*(.*(?:\n[ \t].*)*)\n/) do |name, val|
         | 
| 37 43 | 
             
                    case name
         | 
| 38 44 | 
             
                    when "Attached-File"
         | 
| 39 45 | 
             
                      attached_files.push(val.strip)
         | 
| 40 46 | 
             
                    when "Attached-Message"
         | 
| 41 47 | 
             
                      attached_messages.push(val.strip)
         | 
| 48 | 
            +
                    when "PGP-Sign"
         | 
| 49 | 
            +
                      pgp_sign = val.strip == "yes"
         | 
| 50 | 
            +
                    when "PGP-Encrypt"
         | 
| 51 | 
            +
                      pgp_encrypt = val.strip == "yes"
         | 
| 42 52 | 
             
                    else
         | 
| 43 53 | 
             
                      m[name] = val
         | 
| 44 54 | 
             
                    end
         | 
| @@ -55,32 +65,45 @@ module Mournmail | |
| 55 65 | 
             
                    end
         | 
| 56 66 | 
             
                  end
         | 
| 57 67 | 
             
                  attached_files.each do |file|
         | 
| 58 | 
            -
                    m.add_file(file)
         | 
| 68 | 
            +
                    m.add_file(filename: File.basename(file),
         | 
| 69 | 
            +
                               content: File.read(file),
         | 
| 70 | 
            +
                               encoding: "binary")
         | 
| 59 71 | 
             
                  end
         | 
| 60 | 
            -
                   | 
| 61 | 
            -
             | 
| 72 | 
            +
                  conf = Mournmail.account_config
         | 
| 73 | 
            +
                  m.delivery_method(@buffer[:mournmail_delivery_method] ||
         | 
| 74 | 
            +
                                    conf[:delivery_method],
         | 
| 75 | 
            +
                                    @buffer[:mournmail_delivery_options] ||
         | 
| 76 | 
            +
                                    conf[:delivery_options])
         | 
| 62 77 | 
             
                  bury_buffer(@buffer)
         | 
| 63 78 | 
             
                  background do
         | 
| 64 79 | 
             
                    begin
         | 
| 65 80 | 
             
                      if !attached_messages.empty?
         | 
| 66 81 | 
             
                        attached_messages.each do |attached_message|
         | 
| 67 | 
            -
                           | 
| 68 | 
            -
                          s | 
| 82 | 
            +
                          cache_id = attached_message.strip
         | 
| 83 | 
            +
                          s = File.read(Mournmail.mail_cache_path(cache_id))
         | 
| 69 84 | 
             
                          part = Mail::Part.new(content_type: "message/rfc822", body: s)
         | 
| 70 85 | 
             
                          m.body << part
         | 
| 71 86 | 
             
                        end
         | 
| 72 87 | 
             
                      end
         | 
| 73 | 
            -
                       | 
| 74 | 
            -
             | 
| 75 | 
            -
             | 
| 76 | 
            -
             | 
| 88 | 
            +
                      if pgp_sign || pgp_encrypt
         | 
| 89 | 
            +
                        m.gpg(sign: pgp_sign, encrypt: pgp_encrypt)
         | 
| 90 | 
            +
                      end
         | 
| 91 | 
            +
                      m.deliver
         | 
| 92 | 
            +
                      next_tick do
         | 
| 93 | 
            +
                        message("Mail sent.")
         | 
| 94 | 
            +
                      end
         | 
| 95 | 
            +
                      outbox = Mournmail.account_config[:outbox_mailbox]
         | 
| 96 | 
            +
                      if outbox
         | 
| 97 | 
            +
                        Mournmail.imap_connect do |imap|
         | 
| 98 | 
            +
                          unless imap.list("", outbox)
         | 
| 99 | 
            +
                            imap.create(outbox)
         | 
| 100 | 
            +
                          end
         | 
| 77 101 | 
             
                          imap.append(outbox, m.to_s, [:Seen])
         | 
| 78 102 | 
             
                        end
         | 
| 79 103 | 
             
                      end
         | 
| 80 104 | 
             
                      next_tick do
         | 
| 81 105 | 
             
                        kill_buffer(@buffer, force: true)
         | 
| 82 106 | 
             
                        Mournmail.back_to_summary
         | 
| 83 | 
            -
                        message("Mail sent.")
         | 
| 84 107 | 
             
                      end
         | 
| 85 108 | 
             
                    rescue Exception
         | 
| 86 109 | 
             
                      next_tick do
         | 
| @@ -101,9 +124,7 @@ module Mournmail | |
| 101 124 | 
             
                define_local_command(:draft_attach_file, doc: "Attach a file.") do
         | 
| 102 125 | 
             
                  |file_name = read_file_name("Attach file: ")|
         | 
| 103 126 | 
             
                  @buffer.save_excursion do
         | 
| 104 | 
            -
                     | 
| 105 | 
            -
                    @buffer.re_search_forward(/^--text follows this line--$/)
         | 
| 106 | 
            -
                    @buffer.beginning_of_line
         | 
| 127 | 
            +
                    end_of_header
         | 
| 107 128 | 
             
                    @buffer.insert("Attached-File: #{file_name}\n")
         | 
| 108 129 | 
             
                  end
         | 
| 109 130 | 
             
                end
         | 
| @@ -135,5 +156,27 @@ module Mournmail | |
| 135 156 | 
             
                    @buffer.insert("\t")
         | 
| 136 157 | 
             
                  end
         | 
| 137 158 | 
             
                end
         | 
| 159 | 
            +
                
         | 
| 160 | 
            +
                define_local_command(:draft_pgp_sign, doc: "PGP sign.") do
         | 
| 161 | 
            +
                  @buffer.save_excursion do
         | 
| 162 | 
            +
                    end_of_header
         | 
| 163 | 
            +
                    @buffer.insert("PGP-Sign: yes\n")
         | 
| 164 | 
            +
                  end
         | 
| 165 | 
            +
                end
         | 
| 166 | 
            +
                
         | 
| 167 | 
            +
                define_local_command(:draft_pgp_encrypt, doc: "PGP encrypt.") do
         | 
| 168 | 
            +
                  @buffer.save_excursion do
         | 
| 169 | 
            +
                    end_of_header
         | 
| 170 | 
            +
                    @buffer.insert("PGP-Encrypt: yes\n")
         | 
| 171 | 
            +
                  end
         | 
| 172 | 
            +
                end
         | 
| 173 | 
            +
             | 
| 174 | 
            +
                private
         | 
| 175 | 
            +
             | 
| 176 | 
            +
                def end_of_header
         | 
| 177 | 
            +
                  @buffer.beginning_of_buffer
         | 
| 178 | 
            +
                  @buffer.re_search_forward(/^--text follows this line--$/)
         | 
| 179 | 
            +
                  @buffer.beginning_of_line
         | 
| 180 | 
            +
                end
         | 
| 138 181 | 
             
              end
         | 
| 139 182 | 
             
            end
         | 
| @@ -0,0 +1,73 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "mail"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Mournmail
         | 
| 6 | 
            +
              module MailEncodedWordPatch
         | 
| 7 | 
            +
                private
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                def fold(prepend = 0) # :nodoc:
         | 
| 10 | 
            +
                  charset = normalized_encoding
         | 
| 11 | 
            +
                  decoded_string = decoded.to_s
         | 
| 12 | 
            +
                  if charset != "UTF-8" ||
         | 
| 13 | 
            +
                      decoded_string.ascii_only? ||
         | 
| 14 | 
            +
                      !decoded_string.respond_to?(:encoding) ||
         | 
| 15 | 
            +
                      decoded_string.encoding != Encoding::UTF_8 ||
         | 
| 16 | 
            +
                      Regexp.new('\p{Han}|\p{Hiragana}|\p{Katakana}') !~ decoded_string
         | 
| 17 | 
            +
                    # Use Q encoding
         | 
| 18 | 
            +
                    return super(prepend)
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
                  words = decoded_string.split(/[ \t]/)
         | 
| 21 | 
            +
                  folded_lines   = []
         | 
| 22 | 
            +
                  b_encoding_extra_size = "=?#{charset}?B??=".bytesize
         | 
| 23 | 
            +
                  while !words.empty?
         | 
| 24 | 
            +
                    limit = 78 - prepend
         | 
| 25 | 
            +
                    line = String.new
         | 
| 26 | 
            +
                    fold_line = false
         | 
| 27 | 
            +
                    while !fold_line && !words.empty?
         | 
| 28 | 
            +
                      word = words.first
         | 
| 29 | 
            +
                      s = (line.empty? ? "" : " ").dup
         | 
| 30 | 
            +
                      if word.ascii_only?
         | 
| 31 | 
            +
                        s << word
         | 
| 32 | 
            +
                        break if !line.empty? && line.bytesize + s.bytesize > limit
         | 
| 33 | 
            +
                        words.shift
         | 
| 34 | 
            +
                        if prepend + line.bytesize + s.bytesize > 998
         | 
| 35 | 
            +
                          words.unshift(s.slice!(998 - prepend .. -1))
         | 
| 36 | 
            +
                          fold_line = true
         | 
| 37 | 
            +
                        end
         | 
| 38 | 
            +
                      else
         | 
| 39 | 
            +
                        words.shift
         | 
| 40 | 
            +
                        encoded_text = base64_encode(word)
         | 
| 41 | 
            +
                        min_size = line.bytesize + s.bytesize + b_encoding_extra_size
         | 
| 42 | 
            +
                        new_size = min_size + encoded_text.bytesize
         | 
| 43 | 
            +
                        if new_size > limit
         | 
| 44 | 
            +
                          n = ((limit - min_size) * 3.0 / 4.0).floor
         | 
| 45 | 
            +
                          if n <= 0
         | 
| 46 | 
            +
                            words.unshift(word) if !line.empty?
         | 
| 47 | 
            +
                            break
         | 
| 48 | 
            +
                          end
         | 
| 49 | 
            +
                          truncated = word.byteslice(0, n).scrub("")
         | 
| 50 | 
            +
                          rest = word.byteslice(truncated.bytesize,
         | 
| 51 | 
            +
                                                word.bytesize - truncated.bytesize)
         | 
| 52 | 
            +
                          words.unshift(rest)
         | 
| 53 | 
            +
                          encoded_text = base64_encode(truncated)
         | 
| 54 | 
            +
                          fold_line = true
         | 
| 55 | 
            +
                        end
         | 
| 56 | 
            +
                        encoded_word = "=?#{charset}?B?#{encoded_text}?="
         | 
| 57 | 
            +
                        s << encoded_word
         | 
| 58 | 
            +
                      end
         | 
| 59 | 
            +
                      line << s
         | 
| 60 | 
            +
                    end
         | 
| 61 | 
            +
                    folded_lines << line
         | 
| 62 | 
            +
                    prepend = 1 # Space will be prepended
         | 
| 63 | 
            +
                  end
         | 
| 64 | 
            +
                  folded_lines
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                def base64_encode(word)
         | 
| 68 | 
            +
                  Mail::Encodings::Base64.encode(word).gsub(/[\r\n]/, "")
         | 
| 69 | 
            +
                end
         | 
| 70 | 
            +
              end
         | 
| 71 | 
            +
            end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
            Mail::UnstructuredField.prepend(Mournmail::MailEncodedWordPatch)
         | 
| @@ -13,7 +13,8 @@ module Mournmail | |
| 13 13 |  | 
| 14 14 | 
             
                define_syntax :field_name, /^[A-Za-z\-]+: /
         | 
| 15 15 | 
             
                define_syntax :quotation, /^>.*/
         | 
| 16 | 
            -
                define_syntax :mime_part, | 
| 16 | 
            +
                define_syntax :mime_part,
         | 
| 17 | 
            +
                  /^\[(([0-9.]+) [A-Za-z._\-]+\/[A-Za-z._\-]+.*|PGP\/MIME .*)\]$/
         | 
| 17 18 | 
             
                define_syntax :link, URI_REGEXP
         | 
| 18 19 |  | 
| 19 20 | 
             
                def initialize(buffer)
         | 
| @@ -53,7 +54,7 @@ module Mournmail | |
| 53 54 | 
             
                    if @buffer.looking_at?(/\[([0-9.]+) .*\]/)
         | 
| 54 55 | 
             
                      index = match_string(1)
         | 
| 55 56 | 
             
                      indices = index.split(".").map(&:to_i)
         | 
| 56 | 
            -
                       | 
| 57 | 
            +
                      @buffer[:mournmail_mail].dig_part(*indices)
         | 
| 57 58 | 
             
                    else
         | 
| 58 59 | 
             
                      nil
         | 
| 59 60 | 
             
                    end
         | 
| @@ -1,7 +1,6 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 3 | 
             
            require "mail"
         | 
| 4 | 
            -
            require "mail-iso-2022-jp"
         | 
| 5 4 |  | 
| 6 5 | 
             
            module Mournmail
         | 
| 7 6 | 
             
              module MessageRendering
         | 
| @@ -12,7 +11,7 @@ module Mournmail | |
| 12 11 |  | 
| 13 12 | 
             
                  def render_header
         | 
| 14 13 | 
             
                    CONFIG[:mournmail_display_header_fields].map { |name|
         | 
| 15 | 
            -
                      val = self[name]
         | 
| 14 | 
            +
                      val = self[name]&.to_s&.gsub(/\t/, " ")
         | 
| 16 15 | 
             
                      val ? "#{name}: #{val}\n" : ""
         | 
| 17 16 | 
             
                    }.join
         | 
| 18 17 | 
             
                  end        
         | 
| @@ -20,7 +19,13 @@ module Mournmail | |
| 20 19 | 
             
                  def render_body(indices = [])
         | 
| 21 20 | 
             
                    if HAVE_MAIL_GPG && encrypted?
         | 
| 22 21 | 
             
                      mail = decrypt(verify: true)
         | 
| 23 | 
            -
                       | 
| 22 | 
            +
                      if mail.signatures.empty?
         | 
| 23 | 
            +
                        sig = ""
         | 
| 24 | 
            +
                      else
         | 
| 25 | 
            +
                        sig = "[PGP/MIME signature]\n" +
         | 
| 26 | 
            +
                          signature_of(mail)
         | 
| 27 | 
            +
                      end
         | 
| 28 | 
            +
                      return "[PGP/MIME encrypted message]\n" + mail.render(indices) + sig
         | 
| 24 29 | 
             
                    end
         | 
| 25 30 | 
             
                    if multipart?
         | 
| 26 31 | 
             
                      parts.each_with_index.map { |part, i|
         | 
| @@ -28,11 +33,7 @@ module Mournmail | |
| 28 33 | 
             
                      }.join
         | 
| 29 34 | 
             
                    else
         | 
| 30 35 | 
             
                      s = body.decoded
         | 
| 31 | 
            -
                       | 
| 32 | 
            -
                        s.force_encoding(Encoding::UTF_8).scrub("?")
         | 
| 33 | 
            -
                      else
         | 
| 34 | 
            -
                        s.encode(Encoding::UTF_8, charset, replace: "?")
         | 
| 35 | 
            -
                      end.gsub(/\r\n/, "\n")
         | 
| 36 | 
            +
                      Mournmail.to_utf8(s, charset).gsub(/\r\n/, "\n")
         | 
| 36 37 | 
             
                    end + pgp_signature
         | 
| 37 38 | 
             
                  end
         | 
| 38 39 |  | 
| @@ -54,15 +55,21 @@ module Mournmail | |
| 54 55 | 
             
                  def pgp_signature 
         | 
| 55 56 | 
             
                    if HAVE_MAIL_GPG && signed?
         | 
| 56 57 | 
             
                      verified = verify
         | 
| 57 | 
            -
                       | 
| 58 | 
            -
                      from = verified.signatures.map { |sig|
         | 
| 59 | 
            -
                        sig.from rescue sig.fingerprint
         | 
| 60 | 
            -
                      }.join(", ")
         | 
| 61 | 
            -
                      "#{validity} signature from #{from}\n"
         | 
| 58 | 
            +
                      signature_of(verified)
         | 
| 62 59 | 
             
                    else
         | 
| 63 60 | 
             
                      ""
         | 
| 64 61 | 
             
                    end
         | 
| 65 62 | 
             
                  end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                  def signature_of(m)
         | 
| 65 | 
            +
                    validity = m.signature_valid? ? "Good" : "Bad"
         | 
| 66 | 
            +
                    from = m.signatures.map { |sig|
         | 
| 67 | 
            +
                      sig.from rescue sig.fingerprint
         | 
| 68 | 
            +
                    }.join(", ")
         | 
| 69 | 
            +
                    s = "#{validity} signature from #{from}"
         | 
| 70 | 
            +
                    message(s)
         | 
| 71 | 
            +
                    s + "\n"
         | 
| 72 | 
            +
                  end
         | 
| 66 73 | 
             
                end
         | 
| 67 74 |  | 
| 68 75 | 
             
                refine ::Mail::Part do
         | 
| @@ -75,7 +82,7 @@ module Mournmail | |
| 75 82 |  | 
| 76 83 | 
             
                  def dig_part(i, *rest_indices)
         | 
| 77 84 | 
             
                    if main_type == "message" && sub_type == "rfc822"
         | 
| 78 | 
            -
                      mail = Mail.new(body. | 
| 85 | 
            +
                      mail = Mail.new(body.to_s)
         | 
| 79 86 | 
             
                      mail.dig_part(i, *rest_indices)
         | 
| 80 87 | 
             
                    else
         | 
| 81 88 | 
             
                      part = parts[i]
         | 
| @@ -97,7 +104,7 @@ module Mournmail | |
| 97 104 | 
             
                    elsif main_type == "message" && sub_type == "rfc822"
         | 
| 98 105 | 
             
                      mail = Mail.new(body.raw_source)
         | 
| 99 106 | 
             
                      mail.render(indices)
         | 
| 100 | 
            -
                    elsif  | 
| 107 | 
            +
                    elsif attachment?
         | 
| 101 108 | 
             
                      ""
         | 
| 102 109 | 
             
                    else
         | 
| 103 110 | 
             
                      if main_type == "text" && sub_type == "plain"
         |