gmail 0.4.2 → 0.5.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.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +13 -0
  4. data/.rubocop_todo.yml +239 -0
  5. data/.travis.yml +19 -0
  6. data/CHANGELOG.md +75 -18
  7. data/Gemfile +1 -1
  8. data/README.md +214 -131
  9. data/Rakefile +11 -0
  10. data/gmail.gemspec +8 -8
  11. data/lib/gmail.rb +31 -18
  12. data/lib/gmail/client.rb +11 -7
  13. data/lib/gmail/client/base.rb +52 -48
  14. data/lib/gmail/client/plain.rb +9 -5
  15. data/lib/gmail/client/xoauth.rb +23 -6
  16. data/lib/gmail/client/xoauth2.rb +39 -0
  17. data/lib/gmail/imap_extensions.rb +150 -0
  18. data/lib/gmail/labels.rb +33 -16
  19. data/lib/gmail/mailbox.rb +60 -42
  20. data/lib/gmail/message.rb +142 -101
  21. data/lib/gmail/version.rb +2 -2
  22. data/spec/account.yml.obfus +2 -0
  23. data/spec/gmail/client/base_spec.rb +5 -0
  24. data/spec/gmail/client/plain_spec.rb +168 -0
  25. data/spec/gmail/client/xoauth2_spec.rb +186 -0
  26. data/spec/gmail/client/xoauth_spec.rb +5 -0
  27. data/spec/gmail/client_spec.rb +5 -0
  28. data/spec/gmail/imap_extensions_spec.rb +12 -0
  29. data/spec/gmail/labels_spec.rb +18 -0
  30. data/spec/{mailbox_spec.rb → gmail/mailbox_spec.rb} +14 -14
  31. data/spec/gmail/message_spec.rb +181 -0
  32. data/spec/gmail_spec.rb +21 -21
  33. data/spec/recordings/gmail/_new_connects_with_client_and_give_it_context_when_block_given.yml +28 -0
  34. data/spec/recordings/gmail/_new_connects_with_gmail_service_and_return_valid_connection_object.yml +28 -0
  35. data/spec/recordings/gmail/_new_does_not_raise_error_when_couldn_t_connect_with_given_account.yml +13 -0
  36. data/spec/recordings/gmail/_new_raises_error_when_couldn_t_connect_with_given_account.yml +13 -0
  37. data/spec/recordings/gmail_client_plain/instance/_connection_automatically_logs_in_to_gmail_account_when_it_s_called.yml +42 -0
  38. data/spec/recordings/gmail_client_plain/instance/delivers_inline_composed_email.yml +42 -0
  39. data/spec/recordings/gmail_client_plain/instance/does_not_log_in_when_given_gmail_account_is_invalid.yml +13 -0
  40. data/spec/recordings/gmail_client_plain/instance/does_not_raise_error_even_though_gmail_account_is_invalid.yml +13 -0
  41. data/spec/recordings/gmail_client_plain/instance/labels/checks_if_there_is_given_label_defined.yml +196 -0
  42. data/spec/recordings/gmail_client_plain/instance/labels/creates_given_label.yml +151 -0
  43. data/spec/recordings/gmail_client_plain/instance/labels/removes_existing_label.yml +146 -0
  44. data/spec/recordings/gmail_client_plain/instance/labels/returns_list_of_all_available_labels.yml +113 -0
  45. data/spec/recordings/gmail_client_plain/instance/properly_logs_in_to_valid_gmail_account.yml +42 -0
  46. data/spec/recordings/gmail_client_plain/instance/properly_logs_out_from_gmail.yml +42 -0
  47. data/spec/recordings/gmail_client_plain/instance/properly_switches_to_given_mailbox.yml +109 -0
  48. data/spec/recordings/gmail_client_plain/instance/properly_switches_to_given_mailbox_using_block_style.yml +109 -0
  49. data/spec/recordings/gmail_client_plain/instance/raises_error_when_given_gmail_account_is_invalid_and_errors_enabled.yml +13 -0
  50. data/spec/recordings/gmail_client_xo_auth2/instance/does_not_log_in_when_given_gmail_account_is_invalid.yml +13 -0
  51. data/spec/recordings/gmail_client_xo_auth2/instance/labels/checks_if_there_is_given_label_defined.yml +27 -0
  52. data/spec/recordings/gmail_client_xo_auth2/instance/labels/creates_given_label.yml +39 -0
  53. data/spec/recordings/gmail_client_xo_auth2/instance/labels/removes_existing_label.yml +39 -0
  54. data/spec/recordings/gmail_client_xo_auth2/instance/labels/returns_list_of_all_available_labels.yml +27 -0
  55. data/spec/recordings/gmail_client_xo_auth2/instance/properly_logs_in_to_valid_gmail_account.yml +15 -0
  56. data/spec/recordings/gmail_client_xo_auth2/instance/properly_logs_out_from_gmail.yml +15 -0
  57. data/spec/recordings/gmail_client_xo_auth2/instance/properly_switches_to_given_mailbox.yml +40 -0
  58. data/spec/recordings/gmail_client_xo_auth2/instance/properly_switches_to_given_mailbox_using_block_style.yml +40 -0
  59. data/spec/recordings/gmail_client_xo_auth2/instance/raises_error_when_given_gmail_account_is_invalid_and_errors_enabled.yml +13 -0
  60. data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/all/localizes_into_the_appropriate_label.yml +116 -0
  61. data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/drafts/localizes_into_the_appropriate_label.yml +116 -0
  62. data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/flagged/localizes_into_the_appropriate_label.yml +116 -0
  63. data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/important/localizes_into_the_appropriate_label.yml +116 -0
  64. data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/inbox/localizes_into_the_appropriate_label.yml +42 -0
  65. data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/junk/localizes_into_the_appropriate_label.yml +116 -0
  66. data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/sent/localizes_into_the_appropriate_label.yml +116 -0
  67. data/spec/recordings/gmail_labels/localize/when_given_the_xl_is_tflag/trash/localizes_into_the_appropriate_label.yml +116 -0
  68. data/spec/recordings/gmail_mailbox/instance/counts_all_emails.yml +277 -0
  69. data/spec/recordings/gmail_mailbox/instance/finds_messages.yml +586 -0
  70. data/spec/recordings/gmail_mailbox/on_initialize/sets_client_and_name.yml +42 -0
  71. data/spec/recordings/gmail_mailbox/on_initialize/works_in_inbox_by_default.yml +42 -0
  72. data/spec/recordings/gmail_message/initialize/sets_prefetch_attrs.yml +578 -0
  73. data/spec/recordings/gmail_message/initialize/sets_uid_and_mailbox.yml +580 -0
  74. data/spec/recordings/gmail_message/instance_methods/deletes_itself.yml +637 -0
  75. data/spec/recordings/gmail_message/instance_methods/marks_itself_read.yml +682 -0
  76. data/spec/recordings/gmail_message/instance_methods/marks_itself_unread.yml +686 -0
  77. data/spec/recordings/gmail_message/instance_methods/moves_from_one_tag_to_other.yml +862 -0
  78. data/spec/recordings/gmail_message/instance_methods/removes_a_given_label.yml +776 -0
  79. data/spec/recordings/gmail_message/instance_methods/removes_a_given_label_with_old_method.yml +776 -0
  80. data/spec/recordings/gmail_message/instance_methods/sets_given_label.yml +690 -0
  81. data/spec/recordings/gmail_message/instance_methods/sets_given_label_with_old_method.yml +691 -0
  82. data/spec/spec_helper.rb +34 -10
  83. data/spec/support/imap_mock.rb +129 -0
  84. data/spec/support/obfuscation.rb +52 -0
  85. metadata +79 -30
  86. data/TODO.md +0 -12
  87. data/lib/gmail/client/imap_extensions.rb +0 -54
  88. data/spec/client_spec.rb +0 -178
  89. data/spec/message_spec.rb +0 -51
@@ -0,0 +1,39 @@
1
+ require 'gmail_xoauth'
2
+
3
+ module Gmail
4
+ module Client
5
+ class XOAuth2 < Base
6
+ attr_reader :token
7
+
8
+ def initialize(username, token)
9
+ @token = token
10
+
11
+ super(username, {})
12
+ end
13
+
14
+ def login(raise_errors = false)
15
+ @imap and @logged_in = (login = @imap.authenticate('XOAUTH2', username, token)) && login.name == 'OK'
16
+ rescue Net::IMAP::NoResponseError => e
17
+ if raise_errors
18
+ message = "Couldn't login to given Gmail account: #{username}"
19
+ message += " (#{e.response.data.text.strip})"
20
+ raise(AuthorizationError.new(e.response), message, e.backtrace)
21
+ end
22
+ end
23
+
24
+ def smtp_settings
25
+ [:smtp, {
26
+ :address => GMAIL_SMTP_HOST,
27
+ :port => GMAIL_SMTP_PORT,
28
+ :domain => mail_domain,
29
+ :user_name => username,
30
+ :password => token,
31
+ :authentication => :xoauth2,
32
+ :enable_starttls_auto => true
33
+ }]
34
+ end
35
+ end # XOAuth
36
+
37
+ register :xoauth2, XOAuth2
38
+ end # Client
39
+ end # Gmail
@@ -0,0 +1,150 @@
1
+ module Gmail
2
+ module ImapExtensions
3
+ # Taken from https://github.com/oxos/gmail-oauth-thread-stats/blob/master/gmail_imap_extensions_compatibility.rb
4
+ def self.patch_net_imap_response_parser(klass = Net::IMAP::ResponseParser)
5
+ # https://github.com/ruby/ruby/blob/4d426fc2e03078d583d5d573d4863415c3e3eb8d/lib/net/imap.rb#L2258
6
+ klass.class_eval do
7
+ def msg_att(n = -1)
8
+ match(Net::IMAP::ResponseParser::T_LPAR)
9
+ attr = {}
10
+ while true
11
+ token = lookahead
12
+ case token.symbol
13
+ when Net::IMAP::ResponseParser::T_RPAR
14
+ shift_token
15
+ break
16
+ when Net::IMAP::ResponseParser::T_SPACE
17
+ shift_token
18
+ next
19
+ end
20
+ case token.value
21
+ when /\A(?:ENVELOPE)\z/ni
22
+ name, val = envelope_data
23
+ when /\A(?:FLAGS)\z/ni
24
+ name, val = flags_data
25
+ when /\A(?:INTERNALDATE)\z/ni
26
+ name, val = internaldate_data
27
+ when /\A(?:RFC822(?:\.HEADER|\.TEXT)?)\z/ni
28
+ name, val = rfc822_text
29
+ when /\A(?:RFC822\.SIZE)\z/ni
30
+ name, val = rfc822_size
31
+ when /\A(?:BODY(?:STRUCTURE)?)\z/ni
32
+ name, val = body_data
33
+ when /\A(?:UID)\z/ni
34
+ name, val = uid_data
35
+
36
+ # Gmail extension
37
+ # Cargo-cult code warning: no idea why the regexp works - just copying a pattern
38
+ when /\A(?:X-GM-LABELS)\z/ni
39
+ name, val = x_gm_labels_data
40
+ when /\A(?:X-GM-MSGID)\z/ni
41
+ name, val = uid_data
42
+ when /\A(?:X-GM-THRID)\z/ni
43
+ name, val = uid_data
44
+ # End Gmail extension
45
+
46
+ else
47
+ parse_error("unknown attribute `%s' for {%d}", token.value, n)
48
+ end
49
+ attr[name] = val
50
+ end
51
+ return attr
52
+ end
53
+
54
+ # Based on Net::IMAP#flags_data, but calling x_gm_labels_list to parse labels
55
+ def x_gm_labels_data
56
+ token = match(self.class::T_ATOM)
57
+ name = token.value.upcase
58
+ match(self.class::T_SPACE)
59
+ return name, x_gm_label_list
60
+ end
61
+
62
+ # Based on Net::IMAP#flag_list with a modified Regexp
63
+ # Labels are returned as escape-quoted strings
64
+ # We extract the labels using a regexp which extracts any unescaped strings
65
+ def x_gm_label_list
66
+ if @str.index(/\(([^)]*)\)/ni, @pos)
67
+ resp = extract_labels_response
68
+
69
+ # We need to manually update the position of the regexp to prevent trip-ups
70
+ @pos += resp.length
71
+
72
+ # `resp` will look something like this:
73
+ # ("\\Inbox" "\\Sent" "one's and two's" "some new label" Awesome Ni&APE-os)
74
+ return resp.gsub(/^\s*\(|\)\s*$/, '').scan(/"([^"]*)"|([^\s"]+)/ni).flatten.compact.collect(&:unescape)
75
+ else
76
+ parse_error("invalid label list")
77
+ end
78
+ end
79
+
80
+ # The way Gmail return tokens can cause issues with Net::IMAP's reader,
81
+ # so we need to extract this section manually
82
+ def extract_labels_response
83
+ special, quoted = false, false
84
+ index, paren_count = 0, 0
85
+
86
+ # Start parsing response string for the labels section, parentheses inclusive
87
+ labels_header = "X-GM-LABELS ("
88
+ start = @str.index(labels_header) + labels_header.length - 1
89
+ substr = @str[start..-1]
90
+ substr.each_char do |char|
91
+ index += 1
92
+ case char
93
+ when '('
94
+ paren_count += 1 unless quoted
95
+ when ')'
96
+ paren_count -= 1 unless quoted
97
+ break if paren_count == 0
98
+ when '"'
99
+ quoted = !quoted unless special
100
+ end
101
+ special = (char == '\\' && !special)
102
+ end
103
+ substr[0..index]
104
+ end
105
+ end # class_eval
106
+
107
+ # Add String#unescape
108
+ add_unescape
109
+ end # PNIRP
110
+
111
+ def self.add_unescape(klass = String)
112
+ klass.class_eval do
113
+ # Add a method to string which unescapes special characters
114
+ # We use a simple state machine to ensure that specials are not
115
+ # themselves escaped
116
+ def unescape
117
+ unesc = ''
118
+ special = false
119
+ escapes = { '\\' => '\\',
120
+ '"' => '"',
121
+ 'n' => "\n",
122
+ 't' => "\t",
123
+ 'r' => "\r",
124
+ 'f' => "\f",
125
+ 'v' => "\v",
126
+ '0' => "\0",
127
+ 'a' => "\a"
128
+ }
129
+
130
+ self.each_char do |char|
131
+ if special
132
+ # If in special mode, add in the replaced special char if there's a match
133
+ # Otherwise, add in the backslash and the current character
134
+ unesc << (escapes.keys.include?(char) ? escapes[char] : "\\#{char}")
135
+ special = false
136
+ else
137
+ # Toggle special mode if backslash is detected; otherwise just add character
138
+ if char == '\\'
139
+ special = true
140
+ else
141
+ unesc << char
142
+ end
143
+ end
144
+ end
145
+ unesc
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -3,21 +3,21 @@ module Gmail
3
3
  include Enumerable
4
4
  attr_reader :connection
5
5
  alias :conn :connection
6
-
6
+
7
7
  def initialize(connection)
8
8
  @connection = connection
9
9
  end
10
-
10
+
11
11
  # Get list of all defined labels.
12
12
  def all
13
13
  @list = []
14
-
14
+
15
15
  ## check each item in list for subfolders
16
- conn.list("", "%").each {|l| sublabels_or_label(l)}
17
-
18
- @list.inject([]) do |labels,label|
19
- label[:name].each_line {|l| labels << Net::IMAP.decode_utf7(l) }
20
- labels
16
+ conn.list("", "%").each { |l| sublabels_or_label(l) }
17
+
18
+ @list.inject([]) do |labels, label|
19
+ label[:name].each_line { |l| labels << Net::IMAP.decode_utf7(l) }
20
+ labels
21
21
  end
22
22
  end
23
23
  alias :list :all
@@ -28,35 +28,52 @@ module Gmail
28
28
  @list << label
29
29
  else
30
30
  @list << label
31
- conn.list("#{label.name}/", "%").each {|l| sublabels_or_label(l)}
31
+ conn.list("#{label.name}/", "%").each { |l| sublabels_or_label(l) }
32
32
  end
33
33
  end
34
-
34
+
35
35
  def each(*args, &block)
36
36
  all.each(*args, &block)
37
37
  end
38
-
39
- # Returns +true+ when given label defined.
38
+
39
+ # Returns +true+ when given label defined.
40
40
  def exists?(label)
41
41
  all.include?(label)
42
42
  end
43
43
  alias :exist? :exists?
44
-
44
+
45
45
  # Creates given label in your account.
46
46
  def create(label)
47
47
  !!conn.create(Net::IMAP.encode_utf7(label)) rescue false
48
48
  end
49
49
  alias :new :create
50
50
  alias :add :create
51
-
52
- # Deletes given label from your account.
51
+
52
+ # Deletes given label from your account.
53
53
  def delete(label)
54
54
  !!conn.delete(Net::IMAP.encode_utf7(label)) rescue false
55
55
  end
56
56
  alias :remove :delete
57
-
57
+
58
58
  def inspect
59
59
  "#<Gmail::Labels#{'0x%04x' % (object_id << 1)}>"
60
60
  end
61
+
62
+ # Localizes a specific label flag into a label name
63
+
64
+ # Accepts standard mailbox flags returned by LIST's special-use extension:
65
+ # :Inbox, :All, :Drafts, :Sent, :Trash, :Important, :Junk, :Flagged
66
+ # and their string equivalents. Capitalization agnostic.
67
+ def localize(label)
68
+ type = label.to_sym.capitalize
69
+ if [:All, :Drafts, :Sent, :Trash, :Important, :Junk, :Flagged].include? type
70
+ @mailboxes ||= connection.list("", "*")
71
+ @mailboxes.select { |box| box.attr.include? type }.collect(&:name).compact.uniq.first
72
+ elsif type == :Inbox
73
+ 'INBOX'
74
+ else
75
+ label
76
+ end
77
+ end
61
78
  end # Labels
62
79
  end # Gmail
@@ -9,44 +9,29 @@ module Gmail
9
9
  :flagged => ['FLAGGED'],
10
10
  :unflagged => ['UNFLAGGED'],
11
11
  :starred => ['FLAGGED'],
12
- :unstarred => ['UNFLAGGED'],
12
+ :unstarred => ['UNFLAGGED'],
13
13
  :deleted => ['DELETED'],
14
14
  :undeleted => ['UNDELETED'],
15
15
  :draft => ['DRAFT'],
16
16
  :undrafted => ['UNDRAFT']
17
17
  }
18
-
18
+
19
19
  attr_reader :name
20
- attr_reader :external_name
20
+ attr_reader :encoded_name
21
21
 
22
- def initialize(gmail, name="INBOX")
23
- @name = name
24
- @external_name = Net::IMAP.decode_utf7(name)
22
+ def initialize(gmail, name = "INBOX")
23
+ @name = Net::IMAP.decode_utf7(name)
24
+ @encoded_name = Net::IMAP.encode_utf7(name)
25
25
  @gmail = gmail
26
26
  end
27
27
 
28
- # Returns list of emails which meets given criteria.
29
- #
30
- # ==== Examples
31
- #
32
- # gmail.inbox.emails(:all)
33
- # gmail.inbox.emails(:unread, :from => "friend@gmail.com")
34
- # gmail.inbox.emails(:all, :after => Time.now-(20*24*3600))
35
- # gmail.mailbox("Test").emails(:read)
36
- #
37
- # gmail.mailbox("Test") do |box|
38
- # box.emails(:read)
39
- # box.emails(:unread) do |email|
40
- # ... do something with each email...
41
- # end
42
- # end
43
- def emails(*args, &block)
28
+ def fetch_uids(*args)
44
29
  args << :all if args.size == 0
45
30
 
46
- if args.first.is_a?(Symbol)
31
+ if args.first.is_a?(Symbol)
47
32
  search = MAILBOX_ALIASES[args.shift].dup
48
33
  opts = args.first.is_a?(Hash) ? args.first : {}
49
-
34
+
50
35
  opts[:after] and search.concat ['SINCE', opts[:after].to_imap_date]
51
36
  opts[:before] and search.concat ['BEFORE', opts[:before].to_imap_date]
52
37
  opts[:on] and search.concat ['ON', opts[:on].to_imap_date]
@@ -57,28 +42,66 @@ module Gmail
57
42
  opts[:attachment] and search.concat ['HAS', 'attachment']
58
43
  opts[:search] and search.concat ['BODY', opts[:search]]
59
44
  opts[:body] and search.concat ['BODY', opts[:body]]
45
+ opts[:uid] and search.concat ['UID', opts[:uid]]
46
+ opts[:gm] and search.concat ['X-GM-RAW', opts[:gm]]
60
47
  opts[:query] and search.concat opts[:query]
61
48
 
62
- @gmail.mailbox(name) do
63
- @gmail.conn.uid_search(search).collect do |uid|
64
- message = (messages[uid] ||= Message.new(self, uid))
65
- block.call(message) if block_given?
66
- message
67
- end
68
- end
49
+ @gmail.mailbox(name) {
50
+ @gmail.conn.uid_search(search)
51
+ }
69
52
  elsif args.first.is_a?(Hash)
70
- emails(:all, args.first)
53
+ fetch_uids(:all, args.first)
71
54
  else
72
55
  raise ArgumentError, "Invalid search criteria"
73
56
  end
74
57
  end
75
- alias :mails :emails
58
+
59
+ # Returns list of emails which meets given criteria.
60
+ #
61
+ # ==== Examples
62
+ #
63
+ # gmail.inbox.emails(:all)
64
+ # gmail.inbox.emails(:unread, :from => "friend@gmail.com")
65
+ # gmail.inbox.emails(:all, :after => Time.now-(20*24*3600))
66
+ # gmail.mailbox("Test").emails(:read)
67
+ #
68
+ # gmail.mailbox("Test") do |box|
69
+ # box.emails(:read)
70
+ # box.emails(:unread) do |email|
71
+ # ... do something with each email...
72
+ # end
73
+ # end
74
+ def emails(*args, &block)
75
+ fetch_uids(*args).collect do |uid|
76
+ message = Message.new(self, uid)
77
+ yield(message) if block_given?
78
+ message
79
+ end
80
+ end
76
81
  alias :search :emails
77
82
  alias :find :emails
78
- alias :filter :emails
79
83
 
80
- # This is a convenience method that really probably shouldn't need to exist,
81
- # but it does make code more readable, if seriously all you want is the count
84
+ def emails_in_batches(*args, &block)
85
+ messages = Array.new
86
+
87
+ uids = fetch_uids(*args)
88
+ if uids && uids.any?
89
+ uids.each_slice(100) do |slice|
90
+ @gmail.conn.uid_fetch(slice, Message::PREFETCH_ATTRS).each do |data|
91
+ message = Message.new(self, nil, data)
92
+ yield(message) if block_given?
93
+ messages << message
94
+ end
95
+ end
96
+ end
97
+
98
+ messages
99
+ end
100
+ alias :search_in_batches :emails_in_batches
101
+ alias :find_in_batches :emails_in_batches
102
+
103
+ # This is a convenience method that really probably shouldn't need to exist,
104
+ # but it does make code more readable, if seriously all you want is the count
82
105
  # of messages.
83
106
  #
84
107
  # ==== Examples
@@ -95,13 +118,8 @@ module Gmail
95
118
  @gmail.mailbox(name) { @gmail.conn.expunge }
96
119
  end
97
120
 
98
- # Cached messages.
99
- def messages
100
- @messages ||= {}
101
- end
102
-
103
121
  def inspect
104
- "#<Gmail::Mailbox#{'0x%04x' % (object_id << 1)} name=#{external_name}>"
122
+ "#<Gmail::Mailbox#{'0x%04x' % (object_id << 1)} name=#{name}>"
105
123
  end
106
124
 
107
125
  def to_s
@@ -1,133 +1,167 @@
1
1
  module Gmail
2
2
  class Message
3
+ PREFETCH_ATTRS = ["UID", "ENVELOPE", "BODY.PEEK[]", "FLAGS", "X-GM-LABELS", "X-GM-MSGID", "X-GM-THRID"]
4
+
3
5
  # Raised when given label doesn't exists.
4
- class NoLabelError < Exception; end
5
-
6
- attr_reader :uid
7
-
8
- def initialize(mailbox, uid)
6
+ class NoLabelError < Exception; end
7
+
8
+ def initialize(mailbox, uid, _attrs = nil)
9
9
  @uid = uid
10
10
  @mailbox = mailbox
11
- @gmail = mailbox.instance_variable_get("@gmail") if mailbox
12
- end
13
-
14
- def labels
15
- @gmail.conn.uid_fetch(uid, "X-GM-LABELS")[0].attr["X-GM-LABELS"]
11
+ @gmail = mailbox.instance_variable_get("@gmail") if mailbox # UGLY
12
+ @_attrs = _attrs
16
13
  end
17
-
14
+
18
15
  def uid
19
- @uid ||= @gmail.conn.uid_search(['HEADER', 'Message-ID', message_id])[0]
16
+ @uid ||= fetch("UID")
17
+ end
18
+
19
+ def msg_id
20
+ @msg_id ||= fetch("X-GM-MSGID")
21
+ end
22
+ alias_method :message_id, :msg_id
23
+
24
+ def thr_id
25
+ @thr_id ||= fetch("X-GM-THRID")
26
+ end
27
+ alias_method :thread_id, :thr_id
28
+
29
+ def envelope
30
+ @envelope ||= fetch("ENVELOPE")
31
+ end
32
+
33
+ def message
34
+ @message ||= Mail.new(fetch("BODY[]"))
20
35
  end
21
-
36
+ alias_method :raw_message, :message
37
+
38
+ def flags
39
+ @flags ||= fetch("FLAGS")
40
+ end
41
+
42
+ def labels
43
+ @labels ||= fetch("X-GM-LABELS")
44
+ end
45
+
22
46
  # Mark message with given flag.
23
47
  def flag(name)
24
- !!@gmail.mailbox(@mailbox.name) { @gmail.conn.uid_store(uid, "+FLAGS", [name]) }
48
+ !!@gmail.mailbox(@mailbox.name) do
49
+ @gmail.conn.uid_store(uid, "+FLAGS", [name])
50
+ clear_cached_attributes
51
+ end
25
52
  end
26
-
27
- # Unmark message.
53
+
54
+ # Unmark message.
28
55
  def unflag(name)
29
- !!@gmail.mailbox(@mailbox.name) { @gmail.conn.uid_store(uid, "-FLAGS", [name]) }
56
+ !!@gmail.mailbox(@mailbox.name) do
57
+ @gmail.conn.uid_store(uid, "-FLAGS", [name])
58
+ clear_cached_attributes
59
+ end
30
60
  end
31
-
32
- # Do commonly used operations on message.
61
+
62
+ # Do commonly used operations on message.
33
63
  def mark(flag)
34
64
  case flag
35
- when :read then read!
36
- when :unread then unread!
37
- when :deleted then delete!
38
- when :spam then spam!
65
+ when :read then read!
66
+ when :unread then unread!
67
+ when :deleted then delete!
68
+ when :spam then spam!
39
69
  else
40
70
  flag(flag)
41
71
  end
42
72
  end
43
-
44
- # Mark this message as a spam.
45
- def spam!
46
- move_to('[Gmail]/Spam')
73
+
74
+ # Check whether message is read
75
+ def read?
76
+ flags.include?(:Seen)
47
77
  end
48
-
78
+
49
79
  # Mark as read.
50
80
  def read!
51
81
  flag(:Seen)
52
82
  end
53
-
83
+
54
84
  # Mark as unread.
55
85
  def unread!
56
86
  unflag(:Seen)
57
87
  end
58
-
88
+
89
+ # Check whether message is starred
90
+ def starred?
91
+ flags.include?(:Flagged)
92
+ end
93
+
59
94
  # Mark message with star.
60
95
  def star!
61
- flag('[Gmail]/Starred')
96
+ flag(:Flagged)
62
97
  end
63
-
98
+
64
99
  # Remove message from list of starred.
65
100
  def unstar!
66
- unflag('[Gmail]/Starred')
101
+ unflag(:Flagged)
67
102
  end
68
-
69
- # Move to trash / bin.
70
- def delete!
71
- @mailbox.messages.delete(uid)
72
- flag(:deleted)
73
103
 
74
- # For some, it's called "Trash", for others, it's called "Bin". Support both.
75
- trash = @gmail.labels.exist?('[Gmail]/Bin') ? '[Gmail]/Bin' : '[Gmail]/Trash'
76
- move_to(trash) unless %w[[Gmail]/Spam [Gmail]/Bin [Gmail]/Trash].include?(@mailbox.name)
104
+ # Marking as spam is done by adding the `\Spam` label. To undo this,
105
+ # you just re-apply the `\Inbox` label (see `#unspam!`)
106
+ def spam!
107
+ add_label("\\Spam")
77
108
  end
78
109
 
79
- # Archive this message.
110
+ # Deleting is done by adding the `\Trash` label. To undo this,
111
+ # you just re-apply the `\Inbox` label (see `#undelete!`)
112
+ def delete!
113
+ add_label("\\Trash")
114
+ end
115
+
116
+ # Archiving is done by adding the `\Trash` label. To undo this,
117
+ # you just re-apply the `\Inbox` label (see `#unarchive!`)
80
118
  def archive!
81
- move_to('[Gmail]/All Mail')
82
- end
83
-
84
- # Move to given box and delete from others.
85
- def move_to(name, from=nil)
86
- label(name, from)
87
- delete! if !%w[[Gmail]/Bin [Gmail]/Trash].include?(name)
88
- end
89
- alias :move :move_to
90
-
91
- # Move message to given and delete from others. When given mailbox doesn't
92
- # exist then it will be automaticaly created.
93
- def move_to!(name, from=nil)
94
- label!(name, from) && delete!
95
- end
96
- alias :move! :move_to!
97
-
98
- # Mark this message with given label. When given label doesn't exist then
99
- # it will raise <tt>NoLabelError</tt>.
100
- #
101
- # See also <tt>Gmail::Message#label!</tt>.
102
- def label(name, from=nil)
103
- @gmail.mailbox(Net::IMAP.encode_utf7(from || @mailbox.external_name)) { @gmail.conn.uid_copy(uid, Net::IMAP.encode_utf7(name)) }
104
- rescue Net::IMAP::NoResponseError
105
- raise NoLabelError, "Label '#{name}' doesn't exist!"
106
- end
107
-
108
- # Mark this message with given label. When given label doesn't exist then
109
- # it will be automaticaly created.
110
- #
111
- # See also <tt>Gmail::Message#label</tt>.
112
- def label!(name, from=nil)
113
- label(name, from)
114
- rescue NoLabelError
115
- @gmail.labels.add(Net::IMAP.encode_utf7(name))
116
- label(name, from)
117
- end
118
- alias :add_label :label!
119
- alias :add_label! :label!
120
-
121
- # Remove given label from this message.
122
- def remove_label!(name)
123
- move_to('[Gmail]/All Mail', name)
124
- end
125
- alias :delete_label! :remove_label!
126
-
119
+ remove_label("\\Inbox")
120
+ end
121
+
122
+ def unarchive!
123
+ add_label("\\Inbox")
124
+ end
125
+ alias_method :unspam!, :unarchive!
126
+ alias_method :undelete!, :unarchive!
127
+
128
+ # Move to given box and delete from others.
129
+ # Apply a given label and optionally remove one.
130
+ # TODO: We should probably deprecate this method. It doesn't really add a lot
131
+ # of value, especially since the concept of "moving" a message from one
132
+ # label to another doesn't totally make sense in the Gmail world.
133
+ def move_to(name, from = nil)
134
+ add_label(name)
135
+ remove_label(from) if from
136
+ end
137
+ alias_method :move, :move_to
138
+ alias_method :move!, :move_to
139
+ alias_method :move_to!, :move_to
140
+
141
+ # Use Gmail IMAP Extensions to add a Label to an email
142
+ def add_label(name)
143
+ @gmail.mailbox(@mailbox.name) do
144
+ @gmail.conn.uid_store(uid, "+X-GM-LABELS", [Net::IMAP.encode_utf7(name.to_s)])
145
+ clear_cached_attributes
146
+ end
147
+ end
148
+ alias_method :label, :add_label
149
+ alias_method :label!, :add_label
150
+ alias_method :add_label!, :add_label
151
+
152
+ # Use Gmail IMAP Extensions to remove a Label from an email
153
+ def remove_label(name)
154
+ @gmail.mailbox(@mailbox.name) do
155
+ @gmail.conn.uid_store(uid, "-X-GM-LABELS", [Net::IMAP.encode_utf7(name.to_s)])
156
+ clear_cached_attributes
157
+ end
158
+ end
159
+ alias_method :remove_label!, :remove_label
160
+
127
161
  def inspect
128
- "#<Gmail::Message#{'0x%04x' % (object_id << 1)} mailbox=#{@mailbox.external_name}#{' uid='+@uid.to_s if @uid}#{' message_id='+@message_id.to_s if @message_id}>"
162
+ "#<Gmail::Message#{'0x%04x' % (object_id << 1)} mailbox=#{@mailbox.name}#{' uid=' + @uid.to_s if @uid}#{' message_id=' + @msg_id.to_s if @msg_id}>"
129
163
  end
130
-
164
+
131
165
  def method_missing(meth, *args, &block)
132
166
  # Delegate rest directly to the message.
133
167
  if envelope.respond_to?(meth)
@@ -149,18 +183,25 @@ module Gmail
149
183
  end
150
184
  end
151
185
 
152
- def envelope
153
- @envelope ||= @gmail.mailbox(@mailbox.name) {
154
- @gmail.conn.uid_fetch(uid, "ENVELOPE")[0].attr["ENVELOPE"]
155
- }
156
- end
157
-
158
- def message
159
- @message ||= Mail.new(@gmail.mailbox(@mailbox.name) {
160
- @gmail.conn.uid_fetch(uid, "RFC822")[0].attr["RFC822"] # RFC822
161
- })
186
+ private
187
+
188
+ def clear_cached_attributes
189
+ @_attrs = nil
190
+ @msg_id = nil
191
+ @thr_id = nil
192
+ @envelope = nil
193
+ @message = nil
194
+ @flags = nil
195
+ @labels = nil
162
196
  end
163
- alias_method :raw_message, :message
164
197
 
198
+ def fetch(value)
199
+ @_attrs ||= begin
200
+ @gmail.mailbox(@mailbox.name) do
201
+ @gmail.conn.uid_fetch(uid, PREFETCH_ATTRS)[0]
202
+ end
203
+ end
204
+ @_attrs.attr[value]
205
+ end
165
206
  end # Message
166
207
  end # Gmail