muck-contacts 2.6.1

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.
@@ -0,0 +1,226 @@
1
+ require "cgi"
2
+ require "net/http"
3
+ require "net/https"
4
+ require "uri"
5
+ require "zlib"
6
+ require "stringio"
7
+ require "thread"
8
+ require "erb"
9
+
10
+ class Contacts
11
+ TYPES = {}
12
+ FILETYPES = {}
13
+ VERSION = "1.4.1"
14
+
15
+ class Base
16
+ def initialize(login, password, options={})
17
+ @login = login
18
+ @password = password
19
+ @connections = {}
20
+ @options = options
21
+ connect
22
+ end
23
+
24
+ def connect
25
+ raise AuthenticationError, "Login and password must not be nil, login: #{@login.inspect}, password: #{@password.inspect}" if @login.nil? || @login.empty? || @password.nil? || @password.empty?
26
+ real_connect
27
+ end
28
+
29
+ def connected?
30
+ @cookies && !@cookies.empty?
31
+ end
32
+
33
+ def contacts(options = {})
34
+ return @contacts if @contacts
35
+ if connected?
36
+ url = URI.parse(contact_list_url)
37
+ http = open_http(url)
38
+ resp, data = http.get("#{url.path}?#{url.query}",
39
+ "Cookie" => @cookies
40
+ )
41
+
42
+ if resp.code_type != Net::HTTPOK
43
+ raise ConnectionError, self.class.const_get(:PROTOCOL_ERROR)
44
+ end
45
+
46
+ parse(data, options)
47
+ end
48
+ end
49
+
50
+ def login
51
+ @attempt ||= 0
52
+ @attempt += 1
53
+
54
+ if @attempt == 1
55
+ @login
56
+ else
57
+ if @login.include?("@#{domain}")
58
+ @login.sub("@#{domain}","")
59
+ else
60
+ "#{@login}@#{domain}"
61
+ end
62
+ end
63
+ end
64
+
65
+ def password
66
+ @password
67
+ end
68
+
69
+ def skip_gzip?
70
+ false
71
+ end
72
+ private
73
+
74
+ def domain
75
+ @d ||= URI.parse(self.class.const_get(:URL)).host.sub(/^www\./,'')
76
+ end
77
+
78
+ def contact_list_url
79
+ self.class.const_get(:CONTACT_LIST_URL)
80
+ end
81
+
82
+ def address_book_url
83
+ self.class.const_get(:ADDRESS_BOOK_URL)
84
+ end
85
+
86
+ def open_http(url)
87
+ c = @connections[Thread.current.object_id] ||= {}
88
+ http = c["#{url.host}:#{url.port}"]
89
+ unless http
90
+ http = Net::HTTP.new(url.host, url.port)
91
+ if url.port == 443
92
+ http.use_ssl = true
93
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
94
+ end
95
+ c["#{url.host}:#{url.port}"] = http
96
+ end
97
+ http.start unless http.started?
98
+ http
99
+ end
100
+
101
+ def cookie_hash_from_string(cookie_string)
102
+ cookie_string.split(";").map{|i|i.split("=", 2).map{|j|j.strip}}.inject({}){|h,i|h[i[0]]=i[1];h}
103
+ end
104
+
105
+ def parse_cookies(data, existing="")
106
+ return existing if data.nil?
107
+
108
+ cookies = cookie_hash_from_string(existing)
109
+
110
+ data.gsub!(/ ?[\w]+=EXPIRED;/,'')
111
+ data.gsub!(/ ?expires=(.*?, .*?)[;,$]/i, ';')
112
+ data.gsub!(/ ?(domain|path)=[\S]*?[;,$]/i,';')
113
+ data.gsub!(/[,;]?\s*(secure|httponly)/i,'')
114
+ data.gsub!(/(;\s*){2,}/,', ')
115
+ data.gsub!(/(,\s*){2,}/,', ')
116
+ data.sub!(/^,\s*/,'')
117
+ data.sub!(/\s*,$/,'')
118
+
119
+ data.split(", ").map{|t|t.to_s.split(";").first}.each do |data|
120
+ k, v = data.split("=", 2).map{|j|j.strip}
121
+ if cookies[k] && v.empty?
122
+ cookies.delete(k)
123
+ elsif v && !v.empty?
124
+ cookies[k] = v
125
+ end
126
+ end
127
+
128
+ cookies.map{|k,v| "#{k}=#{v}"}.join("; ")
129
+ end
130
+
131
+ def remove_cookie(cookie, cookies)
132
+ parse_cookies("#{cookie}=", cookies)
133
+ end
134
+
135
+ def post(url, postdata, cookies="", referer="")
136
+ url = URI.parse(url)
137
+ http = open_http(url)
138
+ http_header = { "User-Agent" => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0",
139
+ "Accept-Encoding" => "gzip",
140
+ "Cookie" => cookies,
141
+ "Referer" => referer,
142
+ "Content-Type" => 'application/x-www-form-urlencoded'
143
+ }
144
+ http_header.reject!{|k, v| k == 'Accept-Encoding'} if skip_gzip?
145
+ resp, data = http.post(url.path, postdata, http_header)
146
+ data = uncompress(resp, data)
147
+ cookies = parse_cookies(resp.response['set-cookie'], cookies)
148
+ forward = resp.response['Location']
149
+ forward ||= (data =~ /<meta.*?url='([^']+)'/ ? CGI.unescapeHTML($1) : nil)
150
+ if (not forward.nil?) && URI.parse(forward).host.nil?
151
+ forward = url.scheme.to_s + "://" + url.host.to_s + forward
152
+ end
153
+ return data, resp, cookies, forward
154
+ end
155
+
156
+ def get(url, cookies="", referer="")
157
+ url = URI.parse(url)
158
+ http = open_http(url)
159
+ resp, data = http.get("#{url.path}?#{url.query}",
160
+ "User-Agent" => "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0",
161
+ "Accept-Encoding" => "gzip",
162
+ "Cookie" => cookies,
163
+ "Referer" => referer
164
+ )
165
+ data = uncompress(resp, data)
166
+ cookies = parse_cookies(resp.response['set-cookie'], cookies)
167
+ forward = resp.response['Location']
168
+ if (not forward.nil?) && URI.parse(forward).host.nil?
169
+ forward = url.scheme.to_s + "://" + url.host.to_s + forward
170
+ end
171
+ return data, resp, cookies, forward
172
+ end
173
+
174
+ def uncompress(resp, data)
175
+ case resp.response['content-encoding']
176
+ when 'gzip'
177
+ gz = Zlib::GzipReader.new(StringIO.new(data))
178
+ data = gz.read
179
+ gz.close
180
+ resp.response['content-encoding'] = nil
181
+ # FIXME: Not sure what Hotmail was feeding me with their 'deflate',
182
+ # but the headers definitely were not right
183
+ when 'deflate'
184
+ data = Zlib::Inflate.inflate(data)
185
+ resp.response['content-encoding'] = nil
186
+ end
187
+
188
+ data
189
+ end
190
+ end
191
+
192
+ class ContactsError < StandardError
193
+ end
194
+
195
+ class AuthenticationError < ContactsError
196
+ end
197
+
198
+ class ConnectionError < ContactsError
199
+ end
200
+
201
+ class TypeNotFound < ContactsError
202
+ end
203
+
204
+ def self.new(type, login, password="", secret_key="", options={})
205
+ if !password.nil? && password != '' && !secret_key.nil? && secret_key != ''
206
+ password = Encryptor.decrypt(URI.unescape(password), :key => secret_key)
207
+ end
208
+ if TYPES.include?(type.to_s.intern)
209
+ TYPES[type.to_s.intern].new(login, password, options)
210
+ elsif FILETYPES.include?(type.to_s.intern)
211
+ FILETYPES[type.to_s.intern].new(login)
212
+ else
213
+ raise TypeNotFound, "#{type.inspect} is not a valid type, please choose one of the following: #{TYPES.keys.inspect} or #{FILETYPES.keys.inspect}"
214
+ end
215
+ end
216
+
217
+ def self.guess(login, password, options={})
218
+ TYPES.inject([]) do |a, t|
219
+ begin
220
+ a + t[1].new(login, password, options).contacts
221
+ rescue AuthenticationError
222
+ a
223
+ end
224
+ end.uniq
225
+ end
226
+ end
@@ -0,0 +1,24 @@
1
+ begin
2
+ require 'mini_fb'
3
+ rescue LoadError
4
+ puts "Contact Gem: No mini_fb gem, so no facebook"
5
+ else
6
+ class Contacts
7
+ class Facebook < Base
8
+
9
+ def contacts
10
+ return @contacts if @contacts
11
+ end
12
+
13
+
14
+ def real_connect
15
+ f = MiniFB.get(@password, @login, :type => "friends")
16
+ raise "Didn't find users" unless f.data
17
+ @contacts = f.data.map do |hashie| hashie.values end
18
+ rescue Exception => e
19
+ raise AuthenticationError, "Facebook authentication failed"
20
+ end
21
+ end
22
+ TYPES[:facebook] = Facebook
23
+ end
24
+ end
@@ -0,0 +1,32 @@
1
+ require 'gdata'
2
+
3
+ class Contacts
4
+ class Gmail < Base
5
+
6
+ CONTACTS_SCOPE = 'http://www.google.com/m8/feeds/'
7
+ CONTACTS_FEED = CONTACTS_SCOPE + 'contacts/default/full/?max-results=1000'
8
+
9
+ def contacts
10
+ return @contacts if @contacts
11
+ end
12
+
13
+ def real_connect
14
+ @client = GData::Client::Contacts.new
15
+ @client.clientlogin(@login, @password, @options[:captcha_token], @options[:captcha_response])
16
+
17
+ feed = @client.get(CONTACTS_FEED).to_xml
18
+
19
+ @contacts = feed.elements.to_a('entry').collect do |entry|
20
+ title, email = entry.elements['title'].text, nil
21
+ entry.elements.each('gd:email') do |e|
22
+ email = e.attribute('address').value
23
+ end
24
+ [title, email] unless email.nil?
25
+ end
26
+ @contacts.compact!
27
+ rescue GData::Client::AuthorizationError => e
28
+ raise AuthenticationError, "Username or password are incorrect"
29
+ end
30
+ end
31
+ TYPES[:gmail] = Gmail
32
+ end
@@ -0,0 +1,122 @@
1
+ class Contacts
2
+ class Hotmail < Base
3
+ URL = "https://login.live.com/login.srf?id=2"
4
+ OLD_CONTACT_LIST_URL = "http://%s/cgi-bin/addresses"
5
+ NEW_CONTACT_LIST_URL = "http://%s/mail/GetContacts.aspx"
6
+ CONTACT_LIST_URL = "http://mpeople.live.com/default.aspx?pg=0"
7
+ COMPOSE_URL = "http://%s/cgi-bin/compose?"
8
+ PROTOCOL_ERROR = "Hotmail has changed its protocols, please upgrade this library first. If that does not work, report this error at http://rubyforge.org/forum/?group_id=2693"
9
+ PWDPAD = "IfYouAreReadingThisYouHaveTooMuchFreeTime"
10
+ MAX_HTTP_THREADS = 8
11
+
12
+ def real_connect
13
+ data, resp, cookies, forward = get(URL)
14
+ old_url = URL
15
+ until forward.nil?
16
+ data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
17
+ end
18
+
19
+ postdata = "PPSX=%s&PwdPad=%s&login=%s&passwd=%s&LoginOptions=2&PPFT=%s" % [
20
+ CGI.escape(data.split("><").grep(/PPSX/).first[/=\S+$/][2..-3]),
21
+ PWDPAD[0...(PWDPAD.length-@password.length)],
22
+ CGI.escape(login),
23
+ CGI.escape(password),
24
+ CGI.escape(data.split("><").grep(/PPFT/).first[/=\S+$/][2..-3])
25
+ ]
26
+
27
+ form_url = data.split("><").grep(/form/).first.split[5][8..-2]
28
+ data, resp, cookies, forward = post(form_url, postdata, cookies)
29
+
30
+ old_url = form_url
31
+ until cookies =~ /; PPAuth=/ || forward.nil?
32
+ data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
33
+ end
34
+
35
+ if data.index("The e-mail address or password is incorrect")
36
+ raise AuthenticationError, "Username and password do not match"
37
+ elsif data != ""
38
+ raise AuthenticationError, "Required field must not be blank"
39
+ elsif cookies == ""
40
+ raise ConnectionError, PROTOCOL_ERROR
41
+ end
42
+
43
+ data, resp, cookies, forward = get("http://mail.live.com/mail", cookies)
44
+ until forward.nil?
45
+ data, resp, cookies, forward, old_url = get(forward, cookies, old_url) + [forward]
46
+ end
47
+
48
+ @domain = URI.parse(old_url).host
49
+ @cookies = cookies
50
+ rescue AuthenticationError => m
51
+ if @attempt == 1
52
+ retry
53
+ else
54
+ raise m
55
+ end
56
+ end
57
+
58
+ def contacts(options = {})
59
+ if connected?
60
+ url = URI.parse(contact_list_url)
61
+ data, resp, cookies, forward = get( contact_list_url, @cookies )
62
+
63
+ if resp.code_type != Net::HTTPOK
64
+ raise ConnectionError, self.class.const_get(:PROTOCOL_ERROR)
65
+ end
66
+
67
+ @contacts = []
68
+ build_contacts = []
69
+ go = true
70
+ index = 0
71
+
72
+ while(go) do
73
+ go = false
74
+ url = URI.parse(get_contact_list_url(index))
75
+ http = open_http(url)
76
+ resp, data = http.get(get_contact_list_url(index), "Cookie" => @cookies)
77
+
78
+ email_match_text_beginning = Regexp.escape("http://m.mail.live.com/?rru=compose&amp;to=")
79
+ email_match_text_end = Regexp.escape("&amp;")
80
+ raw_html = resp.body.split("\n").grep(/(?:e|dn)lk[0-9]+/)
81
+ raw_html.delete_at 0
82
+ raw_html.inject('') do |memo, row|
83
+ c_info = row.match(/(e|dn)lk([0-9])+/)
84
+
85
+ # Same contact, or different?
86
+ build_contacts << [] if memo != c_info[2]
87
+
88
+ # Grab info
89
+ case c_info[1]
90
+ when "e" # Email
91
+ build_contacts.last[1] = row.match(/#{email_match_text_beginning}(.*)#{email_match_text_end}/)[1]
92
+ when "dn" # Name
93
+ build_contacts.last[0] = row.match(/<a[^>]*>(.+)<\/a>/)[1]
94
+ end
95
+
96
+ # Set memo to contact id
97
+ c_info[2]
98
+ end
99
+
100
+ go = resp.body.include?("ContactList_next")
101
+ index += 1
102
+ end
103
+
104
+ build_contacts.each do |contact|
105
+ unless contact[1].nil?
106
+ # Only return contacts with email addresses
107
+ contact[1] = CGI::unescape(contact[1])
108
+ contact[1] = contact[1].gsub(/&amp;ru.*/, '').gsub(/%40/, '@')
109
+ @contacts << contact
110
+ end
111
+ end
112
+
113
+ return @contacts
114
+ end
115
+ end
116
+
117
+ def get_contact_list_url(index)
118
+ "http://mpeople.live.com/default.aspx?pg=#{index}"
119
+ end
120
+ end
121
+ TYPES[:hotmail] = Hotmail
122
+ end
@@ -0,0 +1,16 @@
1
+ if !Object.const_defined?('ActiveSupport')
2
+ require 'json'
3
+ end
4
+
5
+ class Contacts
6
+ def self.parse_json( string )
7
+ if Object.const_defined?('ActiveSupport') and
8
+ ActiveSupport.const_defined?('JSON')
9
+ ActiveSupport::JSON.decode( string )
10
+ elsif Object.const_defined?('JSON')
11
+ JSON.parse( string )
12
+ else
13
+ raise 'Contacts requires JSON or Rails (with ActiveSupport::JSON)'
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,31 @@
1
+ begin
2
+ require 'oauth'
3
+ rescue LoadError
4
+ puts "No oauth gem, so no linked_in"
5
+ else
6
+ class Contacts
7
+ class LinkedIn < Base
8
+
9
+ def contacts
10
+ return @contacts if @contacts
11
+ end
12
+
13
+
14
+ def real_connect
15
+ consumer = OAuth::Consumer.new(@options[:app_id], @options[:app_secret])
16
+ access_token = OAuth::AccessToken.new(consumer, @login, @password)
17
+ raw_connections = access_token.get("http://api.linkedin.com/v1/people/~/connections", 'x-li-format' => 'json').body
18
+ parsed_connections = JSON.parse(raw_connections)
19
+ raise "Didn't find users" unless parsed_connections["values"] && parsed_connections["values"].count > 0
20
+ contacts = parsed_connections["values"]
21
+
22
+ @contacts = contacts.map do |contact|
23
+ ["#{contact["firstName"]} #{contact["lastName"]}", contact["id"]]
24
+ end
25
+ rescue Exception => e
26
+ raise AuthenticationError, "linked_in authentication failed"
27
+ end
28
+ end
29
+ TYPES[:linked_in] = LinkedIn
30
+ end
31
+ end