├── Rakefile ├── lib ├── china_sms │ ├── version.rb │ └── service │ │ ├── chanyoo.rb │ │ ├── tui3.rb │ │ ├── luosimao.rb │ │ ├── emay.rb │ │ ├── smsbao.rb │ │ ├── yunpian.rb │ │ └── sendcloud.rb └── china_sms.rb ├── Gemfile ├── .travis.yml ├── spec ├── spec_helper.rb ├── service │ ├── emay_spec.rb │ ├── luosimao_spec.rb │ ├── chanyoo_spec.rb │ ├── sendcloud_spec.rb │ ├── tui3_spec.rb │ ├── smsbao_spec.rb │ └── yunpian_spec.rb └── china_sms_spec.rb ├── bin └── console ├── .gitignore ├── china_sms.gemspec ├── LICENSE.txt └── README.md /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | -------------------------------------------------------------------------------- /lib/china_sms/version.rb: -------------------------------------------------------------------------------- 1 | module ChinaSMS 2 | VERSION = "0.0.7" 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in china_sms.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | rvm: 4 | - 1.9.3 5 | - 2.0.0 6 | 7 | script: 8 | - bundle exec rspec spec 9 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'rubygems' 3 | require 'bundler/setup' 4 | 5 | require 'webmock/rspec' 6 | require 'china_sms' 7 | require 'rspec/its' 8 | 9 | #WebMock.allow_net_connect! 10 | RSpec.configure do |config| 11 | end 12 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "china_sms" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | require 'irb' 10 | 11 | IRB.start 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | 19 | *.swo 20 | *.swp 21 | 22 | # Ignore IDE 23 | .idea 24 | 25 | # Ignore bundler config 26 | .bundle 27 | 28 | #mac os 29 | .DS_Store 30 | ._.DS_Store 31 | 32 | .project 33 | -------------------------------------------------------------------------------- /lib/china_sms/service/chanyoo.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module ChinaSMS 3 | module Service 4 | module Chanyoo 5 | extend self 6 | 7 | URL = "http://api.chanyoo.cn/utf8/interface/send_sms.aspx" 8 | 9 | def to(phone, content, options) 10 | phones = Array(phone).join(',') 11 | res = Net::HTTP.post_form(URI.parse(URL), username: options[:username], password: options[:password], receiver: phones, content: content) 12 | result res.body 13 | end 14 | 15 | def result(body) 16 | code = body.match(/.+result>(.+)\<\/result/)[1] 17 | message = body.match(/.+message>(.+)\<\/message/)[1] 18 | { 19 | success: (code.to_i >= 0), 20 | code: code, 21 | message: message.force_encoding("UTF-8") 22 | } 23 | end 24 | 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/china_sms/service/tui3.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'json' 3 | 4 | module ChinaSMS 5 | module Service 6 | module Tui3 # http://tui3.com/ 7 | extend self 8 | 9 | URL = "http://tui3.com/api/send/" 10 | 11 | def to(phone, content, options) 12 | phones = Array(phone).join(',') 13 | res = Net::HTTP.post_form(URI.parse(URL), k: options[:password], t: phones, c: content, p: 1, r: 'json') 14 | body = res.body 15 | body = "{\"err_code\": 2, \"err_msg\":\"非法apikey:#{options[:password]}\"}" if body == 'invalid parameters' 16 | result JSON[body] 17 | end 18 | 19 | def result(body) 20 | { 21 | success: body['err_code'] == 0, 22 | code: body['err_code'], 23 | message: body['err_msg'] 24 | } 25 | end 26 | 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/china_sms/service/luosimao.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module ChinaSMS 4 | module Service 5 | module Luosimao 6 | extend self 7 | 8 | URL = "https://sms-api.luosimao.com/v1/send.json" 9 | 10 | def to(phone, content, options) 11 | url = URI.parse(URL) 12 | post = Net::HTTP::Post.new(url.path) 13 | post.basic_auth(options[:username], options[:password]) 14 | post.set_form_data({mobile: phone, message: content}) 15 | 16 | socket = Net::HTTP.new(url.host, url.port) 17 | socket.use_ssl = true 18 | response = socket.start {|http| http.request(post) } 19 | result JSON.parse(response.body) 20 | end 21 | 22 | def result(body) 23 | { 24 | success: body['error'] == 0, 25 | code: body['error'], 26 | message: body['msg'] 27 | } 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/service/emay_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'spec_helper' 3 | 4 | describe "Emay" do 5 | 6 | describe "#to" do 7 | let(:username) { 'YOUR_CDKEY' } 8 | let(:password) { 'YOUR_PASSWORD' } 9 | let(:url) { "http://sdkhttp.eucp.b2m.cn/sdkproxy/sendsms.action" } 10 | let(:content) { '测试内容' } 11 | subject { ChinaSMS::Service::Emay.to phone, content, username: username, password: password } 12 | 13 | describe 'single phone' do 14 | let(:phone) { '13552241639' } 15 | 16 | before do 17 | stub_request(:post, url). 18 | with(body: {cdkey: username, password: password, phone: phone, message: content}). 19 | to_return(body: '0') 20 | end 21 | 22 | its([:success]) { should eql true } 23 | its([:code]) { should eql '0' } 24 | end 25 | end 26 | 27 | end 28 | -------------------------------------------------------------------------------- /spec/service/luosimao_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'spec_helper' 3 | 4 | describe "Luosimao" do 5 | 6 | describe "luosimao#to" do 7 | let(:username) { 'api' } 8 | let(:password) { 'password' } 9 | let(:url) { "https://sms-api.luosimao.com/v1/send.json" } 10 | let(:content) { '【畅友短信测试】深圳 Rubyist 活动时间变更到周六下午 3:00,请留意。【19屋】' } 11 | subject { ChinaSMS::Service::Luosimao.to phone, content, username: username, password: password } 12 | 13 | describe 'single phone' do 14 | let(:phone) { '13928935535' } 15 | 16 | before do 17 | stub_request(:post, "https://#{username}:#{password}@sms-api.luosimao.com/v1/send.json"). 18 | with(:body => {"message"=> content, "mobile"=> phone}).to_return(body: '{"error":0,"msg":"ok"}') 19 | end 20 | 21 | its([:success]) { should eql true } 22 | its([:code]) { should eql 0 } 23 | its([:message]) { should eql "ok" } 24 | end 25 | end 26 | 27 | end 28 | -------------------------------------------------------------------------------- /lib/china_sms/service/emay.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module ChinaSMS 3 | module Service 4 | module Emay 5 | extend self 6 | URL = "http://sdkhttp.eucp.b2m.cn/sdkproxy" 7 | 8 | def to(phone, content, options) 9 | phones = Array(phone).join(',') 10 | res = Net::HTTP.post_form(URI.parse("#{URL}/sendsms.action"), cdkey: options[:username], password: options[:password], phone: phones, message: content) 11 | result res.body 12 | end 13 | 14 | def get(options) 15 | # res = Net::HTTP.post_form(URI.parse(GET_URL), cdkey: options[:username], password: options[:password]) 16 | url = "#{URL}/getmo.action?cdkey=#{options[:username]}&password=#{options[:password]}" 17 | res = Net::HTTP.get(URI.parse(url)) 18 | res.body 19 | end 20 | 21 | def result(body) 22 | code = body.match(/.+error>(.+)\<\/error/)[1] 23 | { 24 | success: (code.to_i >= 0), 25 | code: code 26 | } 27 | end 28 | 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/china_sms_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'spec_helper' 3 | 4 | describe "ChinaSMS" do 5 | let(:service) { :tui3 } 6 | let(:username) { 'saberma' } 7 | let(:password) { '666666' } 8 | let(:phone) { '13928452841' } 9 | let(:content) { '活动通知:深圳 Rubyist 活动时间变更到明天下午 7:00,请留意。' } 10 | 11 | context 'with service' do 12 | before { ChinaSMS.use service, username: username, password: password } 13 | 14 | describe "#use" do 15 | subject { ChinaSMS } 16 | its(:username) { should eql "saberma"} 17 | end 18 | 19 | describe "#to" do 20 | subject { ChinaSMS.to(phone, content) } 21 | before { ChinaSMS::Service::Tui3.stub(:to).with(phone, content, username: username, password: password).and_return(success: true, code: 0) } 22 | its([:success]) { should eql true } 23 | its([:code]) { should eql 0 } 24 | end 25 | end 26 | 27 | context 'without service' do 28 | before { ChinaSMS.clear } 29 | 30 | describe '#to' do 31 | it 'should be ignore' do 32 | ChinaSMS.to(phone, content) 33 | end 34 | end 35 | 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /china_sms.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | # coding: utf-8 4 | lib = File.expand_path('../lib', __FILE__) 5 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 6 | require 'china_sms/version' 7 | 8 | Gem::Specification.new do |spec| 9 | spec.name = "china_sms" 10 | spec.version = ChinaSMS::VERSION 11 | spec.authors = ["saberma"] 12 | spec.email = ["mahb45@gmail.com"] 13 | spec.description = %q{a gem for chinese people to send sms} 14 | spec.summary = %q{a gem for chinese people to send sms} 15 | spec.homepage = "https://github.com/saberma/china_sms" 16 | spec.license = "MIT" 17 | 18 | spec.files = `git ls-files`.split($/) 19 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 20 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 21 | spec.require_paths = ["lib"] 22 | 23 | spec.add_development_dependency "bundler", "~> 1.2" 24 | spec.add_development_dependency "rake" 25 | spec.add_development_dependency "rspec" 26 | spec.add_development_dependency "rspec-its" 27 | spec.add_development_dependency "webmock" 28 | end 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 saberma 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/china_sms.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require "china_sms/version" 3 | require 'net/http' 4 | Dir.glob("#{File.expand_path(File.dirname(__FILE__))}/china_sms/service/*.rb").sort.each do |f| 5 | require f.match(/(china_sms\/service\/.*)\.rb$/)[0] 6 | end 7 | 8 | module ChinaSMS 9 | extend self 10 | 11 | attr_reader :username, :password 12 | 13 | def use(service, options) 14 | @service = ChinaSMS::Service.const_get("#{service.to_s.capitalize}") 15 | @service.const_set("URL", options[:base_uri]) if options[:base_uri] 16 | @username = options[:username] 17 | @password = options[:password] 18 | end 19 | 20 | def to(receiver, content, options = {}) 21 | options = default_options.merge options 22 | @service.to receiver, content, options if @service 23 | end 24 | 25 | def get(options = {}) 26 | options = default_options.merge options 27 | @service.get options if @service 28 | end 29 | 30 | def clear 31 | @service = @username = @password = nil 32 | end 33 | 34 | private 35 | 36 | def default_options 37 | { 38 | username: @username, 39 | password: @password 40 | } 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /lib/china_sms/service/smsbao.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module ChinaSMS 3 | module Service 4 | module Smsbao 5 | extend self 6 | 7 | URL = "http://api.smsbao.com/sms" 8 | REMAIN_URL = "http://www.smsbao.com/query" 9 | 10 | MESSAGES = { 11 | '0' => '短信发送成功', 12 | '30' => '密码错误', 13 | '40' => '账号不存在', 14 | '41' => '余额不足', 15 | '42' => '帐号过期', 16 | '43' => 'IP地址限制', 17 | '50' => '内容含有敏感词', 18 | '51' => '手机号码不正确' 19 | } 20 | 21 | def to(phone, content, options) 22 | phones = Array(phone).join(',') 23 | res = Net::HTTP.post_form(URI.parse(URL), u: options[:username], p: Digest::MD5.hexdigest(options[:password]), m: phones, c: content) 24 | result res.body 25 | end 26 | 27 | def get options 28 | res = Net::HTTP.post_form(URI.parse(REMAIN_URL), u: options[:username], p: Digest::MD5.hexdigest(options[:password])) 29 | r = res.body.match(/(\d+)\n(\d+),(\d+)/) 30 | { 31 | success: (r[1] == '0'), 32 | code: r[1], 33 | message: MESSAGES[r[1]], 34 | send: r[2].to_i, 35 | remain: r[3].to_i 36 | } 37 | end 38 | 39 | def result(code) 40 | { 41 | success: (code == '0'), 42 | code: code, 43 | message: MESSAGES[code] 44 | } 45 | end 46 | 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/service/chanyoo_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'spec_helper' 3 | 4 | describe "Chanyoo" do 5 | 6 | describe "#to" do 7 | let(:username) { 'saberma' } 8 | let(:password) { '666666' } 9 | let(:url) { "http://api.chanyoo.cn/utf8/interface/send_sms.aspx" } 10 | let(:content) { '【畅友短信测试】深圳 Rubyist 活动时间变更到周六下午 3:00,请留意。【19屋】' } 11 | subject { ChinaSMS::Service::Chanyoo.to phone, content, username: username, password: password } 12 | 13 | describe 'single phone' do 14 | let(:phone) { '13928452841' } 15 | 16 | before do 17 | stub_request(:post, url). 18 | with(body: {username: username, password: password, receiver: phone, content: content}). 19 | to_return(body: '50短信发送成功') 20 | end 21 | 22 | its([:success]) { should eql true } 23 | its([:code]) { should eql '50' } 24 | its([:message]) { should eql "短信发送成功" } 25 | end 26 | 27 | describe 'multiple phones' do 28 | let(:phone) { ['13928452841', '13590142385'] } 29 | 30 | before do 31 | stub_request(:post, url). 32 | with(body: {username: username, password: password, receiver: phone.join(','), content: content}). 33 | to_return(body: '50短信发送成功') 34 | end 35 | 36 | its([:success]) { should eql true } 37 | its([:code]) { should eql '50' } 38 | its([:message]) { should eql "短信发送成功" } 39 | end 40 | 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/service/sendcloud_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'spec_helper' 3 | 4 | describe "Sendcloud" do 5 | let(:username) { 'user' } 6 | let(:password) { 'xxxxtemrejmijfeijixwewe' } 7 | 8 | describe "#to" do 9 | let(:url) { 'http://www.sendcloud.net/smsapi/send' } 10 | let(:template_id) { 3126 } 11 | let(:content) { {} } 12 | subject { ChinaSMS::Service::Sendcloud.to phone, '', {content: content, template_id: template_id , username: username, password: password} } 13 | 14 | describe 'single phone' do 15 | let(:phone) { '13601647905' } 16 | 17 | before do 18 | body = {"phone"=> phone, "vars"=> content.to_json, "msgType"=> '0', "smsUser"=> username, "templateId"=>template_id, "signature"=>"16f795fd3514ed89bdd9fe577fb2601a" } 19 | stub_request(:post, url). 20 | with(body: URI.encode_www_form(body)). 21 | to_return(body: "{\"info\":{\"successCount\":1,\"smsIds\":[\"1478579653941_49236_566_3126_clrsg5$13601647905\"]},\"statusCode\":200,\"message\":\"请求成功\",\"result\":true}") 22 | end 23 | 24 | its([:success]) { should eql true } 25 | its([:code]) { should eql 200 } 26 | its([:message]) { should eql "请求成功" } 27 | end 28 | end 29 | 30 | describe "#get_signature" do 31 | let(:phone) { "13601647905" } 32 | let(:content) { {}.to_json } 33 | let(:params) { {"phone"=> phone, "vars"=> content, "msgType"=>0, "smsUser"=> username, "templateId"=>3126, 'password'=> password} } 34 | subject { ChinaSMS::Service::Sendcloud.get_signature params } 35 | 36 | it { should eql "16f795fd3514ed89bdd9fe577fb2601a" } 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/service/tui3_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'spec_helper' 3 | 4 | describe "Tui3" do 5 | describe "#to" do 6 | let(:password) { '50e7a3smdl521942ea38dnf4b58c5e6b' } 7 | let(:url) { "http://tui3.com/api/send/" } 8 | let(:content) { '推立方测试:深圳 Rubyist 活动时间变更到明天下午 7:00,请留意。' } 9 | subject { ChinaSMS::Service::Tui3.to phone, content, password: password } 10 | 11 | describe 'single phone' do 12 | let(:phone) { '13928452888' } 13 | 14 | before do 15 | stub_request(:post, url). 16 | with(body: {k: password, t: phone, c: content, p: '1', r: 'json'}). 17 | to_return(body: '{"err_code":0,"err_msg":"操作成功!","server_time":"2013-07-01 21:42:37"}' ) 18 | end 19 | 20 | its([:success]) { should eql true } 21 | its([:code]) { should eql 0 } 22 | its([:message]) { should eql "操作成功!" } 23 | end 24 | 25 | describe 'multiple phones' do 26 | let(:phone) { ['13928452888', '13590142385'] } 27 | 28 | before do 29 | stub_request(:post, url). 30 | with(body: {k: password, t: phone.join(','), c: content, p: '1', r: 'json'}). 31 | to_return(body: '{"err_code":0,"err_msg":"操作成功!","server_time":"2013-07-01 21:42:37"}' ) 32 | end 33 | 34 | its([:success]) { should eql true } 35 | its([:code]) { should eql 0 } 36 | its([:message]) { should eql "操作成功!" } 37 | end 38 | 39 | context 'invalid key' do 40 | let(:phone) { '13928452888' } 41 | let(:password) { '666666' } 42 | 43 | before do 44 | stub_request(:post, url). 45 | with(body: {k: password, t: phone, c: content, p: '1', r: 'json'}). 46 | to_return(body: 'invalid parameters' ) 47 | end 48 | 49 | its([:success]) { should eql false } 50 | its([:message]) { should eql "非法apikey:#{password}" } 51 | end 52 | 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/china_sms/service/yunpian.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module ChinaSMS 3 | module Service 4 | module Yunpian 5 | extend self 6 | 7 | GET_URL = "http://yunpian.com/v1/user/get.json" 8 | SEND_URL = 'http://yunpian.com/v1/sms/send.json' 9 | TPL_SEND_URL = 'http://yunpian.com/v1/sms/tpl_send.json' 10 | 11 | def to phone, content, options = {} 12 | options[:tpl_id] ||= 2 13 | options[:apikey] ||= options[:password] 14 | except! options, :username, :password 15 | 16 | res = if content.is_a? Hash 17 | message = parse_content content 18 | options.merge!({ mobile: phone, tpl_value: message }) 19 | Net::HTTP.post_form(URI.parse(TPL_SEND_URL), options) 20 | else 21 | except! options, :tpl_id 22 | message = content 23 | options.merge!({ mobile: phone, text: message }) 24 | Net::HTTP.post_form(URI.parse(SEND_URL), options) 25 | end 26 | 27 | result res.body 28 | end 29 | 30 | def get options = {} 31 | options[:apikey] ||= options[:password] 32 | except! options, :username, :password 33 | res = Net::HTTP.post_form(URI.parse(GET_URL), options) 34 | result res.body 35 | end 36 | 37 | def result body 38 | begin 39 | JSON.parse body 40 | rescue => e 41 | { 42 | code: 502, 43 | msg: "内容解析错误", 44 | detail: e.to_s 45 | } 46 | end 47 | end 48 | 49 | private 50 | 51 | def except! options = {}, *keys 52 | keys.each {|key| options.delete(key)} 53 | options 54 | end 55 | 56 | def parse_content content 57 | content.map { |k, v| "##{k}#=#{v}" }.join('&') 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/service/smsbao_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'spec_helper' 3 | 4 | describe "Smsbao" do 5 | let(:username) { 'saberma' } 6 | let(:password) { '666666' } 7 | describe "#to" do 8 | let(:url) { "http://api.smsbao.com/sms" } 9 | let(:content) { '【短信宝测试】深圳 Rubyist 活动时间变更到明天下午 7:00,请留意。【19屋】' } 10 | subject { ChinaSMS::Service::Smsbao.to phone, content, username: username, password: password } 11 | 12 | describe 'single phone' do 13 | let(:phone) { '13928452841' } 14 | 15 | before do 16 | stub_request(:post, url). 17 | with(body: {u: username, p: Digest::MD5.hexdigest(password), m: phone, c: content}). 18 | to_return(body: '0') 19 | end 20 | 21 | its([:success]) { should eql true } 22 | its([:code]) { should eql '0' } 23 | its([:message]) { should eql "短信发送成功" } 24 | end 25 | 26 | describe 'multiple phones' do 27 | let(:phone) { ['13928452841', '13590142385'] } 28 | 29 | before do 30 | stub_request(:post, url). 31 | with(body: {u: username, p: Digest::MD5.hexdigest(password), m: phone.join(','), c: content}). 32 | to_return(body: '0') 33 | end 34 | 35 | its([:success]) { should eql true } 36 | its([:code]) { should eql '0' } 37 | its([:message]) { should eql "短信发送成功" } 38 | end 39 | 40 | end 41 | 42 | describe "get" do 43 | let(:remain_url) { "http://www.smsbao.com/query" } 44 | subject { ChinaSMS::Service::Smsbao.get username: username, password: password } 45 | 46 | describe "remain send count" do 47 | before do 48 | stub_request(:post, remain_url). 49 | with(body: {u: username, p: Digest::MD5.hexdigest(password)}). 50 | to_return(body: "0\n100,200") 51 | end 52 | 53 | its([:success]) { should eql true } 54 | its([:code]) { should eql '0' } 55 | its([:message]) { should eql '短信发送成功' } 56 | its([:send]) { should eql 100 } 57 | its([:remain]) { should eql 200 } 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChinaSMS 短信平台 Ruby 接口 2 | 3 | [![Build Status](https://travis-ci.org/saberma/china_sms.png?branch=master)](https://travis-ci.org/saberma/china_sms) 4 | 5 | ## 支持以下短信平台 6 | 7 | 所有短信平台都禁止发送私人短信,并要求在短信内容末尾加上签名后缀,如【19屋】 8 | 9 | * [云片网](http://www.yunpian.com/) 专注于帮助企业与客户更好的沟通, 提供短信服务, 功能强大, 可以配置多种[模板](http://www.yunpian.com/api/tpl.html), 实时性高, 定制型强, 价格优惠. 10 | * [推立方](http://tui3.com/) 专注于注册校验码等实时应用,需要[配置内容格式和签名](http://www.tui3.com/Members/smsconfigv2/),会自动在短信加上签名后缀。短信计数:移动/联通每条短信的最大长度为64字符,电信每条最大长度60字符(半角、全角各算一个)。超过该长度后,短信后面自动增加分页信息(x/y),此时,每条短信最大长度需要再减3(不超过10页) 11 | * [短信宝](http://www.smsbao.com/) 12 | * [畅友网络](http://www.chanyoo.cn/) 群发短信需要半小时左右的时间审核,星期五等繁忙时段会有几个小时的延时,不适合发送注册校验码等实时短信,单次最多发送500个号码 13 | * [亿美软通](http://www.emay.cn/) 14 | * [螺丝帽](http://luosimao.com/) 15 | 16 | 感谢 [云片网](http://yunpian.com/?ref=china_sms) 为 [19屋活动平台](http://19wu.com) 提供短信赞助。 17 | 18 | ## 安装 19 | 20 | 加入以下代码到 Gemfile: 21 | 22 | gem 'china_sms' 23 | 24 | 然后执行: 25 | 26 | $ bundle 27 | 28 | 或者直接安装: 29 | 30 | $ gem install china_sms 31 | 32 | ## 使用 33 | 34 | ```ruby 35 | # 支持 :tui3, :yunpian, :smsbao, :chanyoo, :emay, luosimao 短信接口 36 | ChinaSMS.use :tui3, username: 'YOUR_USERNAME', password: 'YOUR_PASSWORD' 37 | ChinaSMS.to '13912345678', '[Test]China SMS gem has been released.' 38 | 39 | 40 | # :yunpian 41 | # 如果content(第二个参数) 是字符串 42 | # 调用 通用接口 发送短信 43 | # 如果是 Hash 44 | # 调用 模板接口 发送短信 45 | # 可选参数: 46 | # :tpl_id 默认是 2 47 | 48 | ChinaSMS.use :yunpian, password: 'YOUR_API_KEY' 49 | 50 | # 通用接口 51 | ChinaSMS.to '13912345678', '[Test]China SMS gem has been released.' 52 | ChinaSMS.to '13912345678', 'China SMS gem has been released.【Test】' # luosimao 的签名要放在后面 53 | 54 | # 模板接口 55 | # 模板是 "您的验证码是#code#【#company#】” 56 | tpl_params = { code: 123, company: '19wu' } 57 | ChinaSMS.to '13912345678', tpl_params, tpl_id: 1 58 | 59 | ``` 60 | 61 | ## 贡献 62 | 63 | ```bash 64 | git clone git@github.com:saberma/china_sms.git 65 | cd china_sms 66 | bundle console # 请不要使用 irb,可能会有依赖问题 67 | ``` 68 | 69 | ## TODO 70 | 71 | * 签名作为参数,根据各个服务提供商的不同来确定放在内容前面还是后面,例如 luosimao 要求放在内容后面 72 | 73 | ## 安全性 74 | 75 | 在安全性方面,很多接口都是使用用户登录明文密码,而云片网、螺丝帽、推立方 和 短信宝 要好一些。 76 | 77 | * **云片网**,使用HTTPS,API使用独立的apikey,不使用登录名和密码,支持 IP 白名单 78 | * **推立方**,不使用登录密码,而是由系统自动生成一长串 API_KEY,专用于接口调用 79 | * **短信宝**,使用登录密码,但在调用时要先转成 MD5 80 | * **螺丝帽**,使用HTTPS,登录密码 81 | -------------------------------------------------------------------------------- /lib/china_sms/service/sendcloud.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module ChinaSMS 3 | module Service 4 | module Sendcloud 5 | extend self 6 | 7 | URL = 'http://www.sendcloud.net/smsapi/send'.freeze 8 | 9 | MESSAGES = { 10 | '500' => '发送失败, 手机空号', 11 | '510' => '发送失败, 手机停机', 12 | '550' => '发送失败, 该模板内容被投诉', 13 | '580' => '发送失败, 手机关机', 14 | '590' => '发送失败, 其他原因', 15 | '200' => '请求成功', 16 | '311' => '部分号码请求成功', 17 | '312' => '全部请求失败', 18 | '411' => '手机号不能为空', 19 | '412' => '手机号格式错误', 20 | '413' => '有重复的手机号', 21 | '421' => '签名参数错误', 22 | '422' => '签名错误', 23 | '431' => '模板不存在', 24 | '432' => '模板未提交审核或者未审核通过', 25 | '433' => '模板ID不能为空', 26 | '441' => '替换变量格式错误', 27 | '451' => '定时发送时间的格式错误', 28 | '452' => '定时发送时间早于服务器时间, 时间已过去', 29 | '461' => '时间戳无效, 与服务器时间间隔大于6秒', 30 | '471' => 'smsUser不存在', 31 | '472' => 'smsUser不能为空', 32 | '473' => '没有权限, 免费用户不能发送短信', 33 | '474' => '用户不存在', 34 | '481' => '手机号和替换变量不能为空', 35 | '482' => '手机号和替换变量格式错误', 36 | '483' => '替换变量长度不能超过32个字符', 37 | '496' => '一分钟内给当前手机号发信太多', 38 | '498' => '一天内给当前手机号发信太多', 39 | '499' => '账户额度不够', 40 | '501' => '服务器异常', 41 | '601' => '你没有权限访问' 42 | } 43 | 44 | module MSG_TYPE 45 | SMS = 0 46 | 47 | MMS = 1 48 | end 49 | 50 | def get_signature(params) 51 | param_str = [] 52 | password = params.delete 'password' 53 | params.keys.sort.each do |key| 54 | param_str << "#{key}=#{params[key]}" 55 | end 56 | 57 | sign_str = "#{password}&#{param_str.join('&')}&#{password}" 58 | 59 | Digest::MD5.hexdigest sign_str 60 | end 61 | 62 | def to(phone, content = '', options = {}) 63 | options[:type] ||= MSG_TYPE::SMS 64 | 65 | params = { 66 | 'phone' => phone, 67 | 'vars' => options[:content].to_json, 68 | 'msgType' => options[:type], 69 | 'smsUser' => options[:username], 70 | 'templateId' => options[:template_id], 71 | 'password' => options[:password] 72 | } 73 | 74 | params.merge! 'signature' => get_signature(params) 75 | 76 | puts "Request params: #{params}" 77 | 78 | res = Net::HTTP.post_form(URI.parse(URL), params) 79 | 80 | puts "Response Body: #{res.body}" 81 | 82 | result JSON.parse(res.body) 83 | end 84 | 85 | def result(response) 86 | code = response['statusCode'] 87 | 88 | { 89 | success: (code == 200), 90 | code: code, 91 | message: response['message'] 92 | } 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /spec/service/yunpian_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'spec_helper' 3 | 4 | describe "Yunpian" do 5 | describe "#to" do 6 | let(:apikey) { '2022b1599967a8cb788c05ddd9fc339e' } 7 | let(:send_url) { "http://yunpian.com/v1/sms/send.json" } 8 | let(:tpl_send_url) { "http://yunpian.com/v1/sms/tpl_send.json" } 9 | let(:content) { '云片测试:验证码 1234。' } 10 | let(:tpl_content) { Hash[:code, 123, :company, '云片网'] } 11 | 12 | describe 'single phone' do 13 | let(:phone) { '13928452841' } 14 | 15 | before do 16 | stub_request(:post, tpl_send_url). 17 | with( 18 | body: { 19 | "apikey" => apikey, 20 | "mobile" => phone, 21 | "tpl_id" => "2", 22 | "tpl_value" => "#code#=123&#company#=云片网" }, 23 | headers: { 24 | 'Content-Type' => 'application/x-www-form-urlencoded' 25 | }). 26 | to_return( 27 | body: { 28 | 'code' => 0, 29 | 'msg' => 'OK', 30 | 'result' => { 31 | 'count' => '1', 32 | 'fee' => '1', 33 | 'sid' => '592762800' 34 | } 35 | }.to_json 36 | ) 37 | 38 | stub_request(:post, send_url). 39 | with( 40 | body: { 41 | "apikey" => apikey, 42 | "mobile" => phone, 43 | "text" => content }, 44 | headers: { 45 | 'Content-Type' => 'application/x-www-form-urlencoded' 46 | }). 47 | to_return( 48 | body: { 49 | 'code' => 0, 50 | 'msg' => 'OK', 51 | 'result' => { 52 | 'count' => '1', 53 | 'fee' => '1', 54 | 'sid' => '592762800' 55 | } 56 | }.to_json 57 | ) 58 | end 59 | 60 | context 'string content' do 61 | subject { ChinaSMS::Service::Yunpian.to phone, content, password: apikey } 62 | 63 | its(["code"]) { should eql 0 } 64 | its(["msg"]) { should eql "OK" } 65 | end 66 | 67 | context 'tpl content' do 68 | subject { ChinaSMS::Service::Yunpian.to phone, tpl_content, password: apikey } 69 | 70 | its(["code"]) { should eql 0 } 71 | its(["msg"]) { should eql "OK" } 72 | end 73 | end 74 | 75 | describe 'invalid key' do 76 | let(:phone) { '13928452841' } 77 | let(:apikey) { '666666' } 78 | 79 | before do 80 | stub_request(:post, tpl_send_url). 81 | with( 82 | body: { 83 | "apikey" => apikey, 84 | "mobile" => phone, 85 | "tpl_id" => "2", 86 | "tpl_value" => "#code#=123&#company#=云片网" }, 87 | headers: { 88 | 'Content-Type' => 'application/x-www-form-urlencoded' 89 | }). 90 | to_return( 91 | body: { 92 | "code" => -1, 93 | "msg" => "非法的apikey", 94 | "detail" => "请检查的apikey是否正确" 95 | }.to_json 96 | ) 97 | 98 | stub_request(:post, send_url). 99 | with( 100 | body: { 101 | "apikey" => apikey, 102 | "mobile" => phone, 103 | "text" => content }, 104 | headers: { 105 | 'Content-Type' => 'application/x-www-form-urlencoded' 106 | }). 107 | to_return( 108 | body: { 109 | "code" => -1, 110 | "msg" => "非法的apikey", 111 | "detail" => "请检查的apikey是否正确" 112 | }.to_json 113 | ) 114 | end 115 | 116 | context 'string content' do 117 | subject { ChinaSMS::Service::Yunpian.to phone, content, password: apikey } 118 | 119 | its(["code"]) { should eql -1 } 120 | its(["msg"]) { should eql "非法的apikey" } 121 | its(["detail"]) { should eql "请检查的apikey是否正确" } 122 | end 123 | 124 | context 'tpl content' do 125 | subject { ChinaSMS::Service::Yunpian.to phone, tpl_content, password: apikey } 126 | 127 | its(["code"]) { should eql -1 } 128 | its(["msg"]) { should eql "非法的apikey" } 129 | its(["detail"]) { should eql "请检查的apikey是否正确" } 130 | end 131 | end 132 | 133 | end 134 | end 135 | --------------------------------------------------------------------------------