muck-contacts 2.6.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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