Главная > Программирование, сетевое > Python: Бесплатная отправка SMS через mail.ru.

Python: Бесплатная отправка SMS через mail.ru.

В mail.ru агенте есть возможно одна полезная вещь - отправка до 50 sms в сутки с одного аккаунта. Протокол агента является открытым и его можно посмотреть на сайте, однако выложена не совсем свежая версия и не сказано ни слова об отправке sms, но это не проблема.

И так, смотрим описание протокола. Для начала нам надо подключится к mrim.mail.ru на порт 2042 или 443 и адрес сервера агента и порт в обычном текстовом формате "ip:port". Затем надо отправить пакет MRIM_CS_HELLO. Как сказано в описании, заголовок пакета имеет следующий формат:


u_long magic;
u_long proto;
u_long seq;
u_long msg;
u_long dlen;
u_long from;
u_long fromport;
u_char reserved[16];

Размер u_long'а - 4 байта. Первое значение magic соответствует числу 0xDEADBEEF - своеобразный юмор разработчиков, если смотреть на это значение как на строку, то перевод будет что-то вроде "мёртвая говядина". Однако разработчики mail.ru пошли дальше, что бы нас ещё больше рассмешить они решили отправлять все числовые значения задом наперёд, т.е. нам надо записать в заголовок не 0xDEADBEEF, а 0xEFBEADDE (пишем последний байт - EF, предпоследний - BE и т.д.). Строковый переменные отправляются в нормальном порядке. Всё это можно узнать запустив какой-нибудь снифер и посмотрев что шлёт официальный агент.

В итоге, что мы должны послать на сервер:
magic = 0xEFBEADDE (соответствует 0xDEADBEEF)
proto = 0x07000100 (первые два байта - старшая версия протокола, вторые - младшая, 0x00010007)
seq = 0x01000000 (соответствует 0x000001)
msg = 0x01100000 (MRIM_CS_HELLO - 0x00001001)
dlen = 0x00000000 (длинна данных)
from = 0x00000000 (ip адрес клиента, указывать не обязательно)
fromport = 0x000000 (порт клиента, указывать не обязательно)
reserved[16] = 0x00000000000000000000000000000000 (зарезервировано)

В ответ мы должны получить пакет с тем же номером последовательности (seq) и типом (msg) MRIM_CS_HELLO_ACK.

Оформим всё это в виде кода. Создаём файл packets.py в котором будут храниться определённые протоколом константы и класс packet с методами:

  • setHost(host, port) для установки адреса и порта с которого происходит подключение к серверу (этот метод не очень то нужен);
  • getPacket(msg) - устанавливает тип пакета равный MSG, увеличивает текущий номер последовательности на 1 и возвращает готовый пакет для отправки в текстовом формате;
  • setPacket(p) - метод устанавливает из полученного пакета все свойства в классе, если в p содержится поле данных, то оно записывается в свойство data;
  • addRawData(data) добавляет к свойству data переданный параметр data и обновляет значение длины данных;
  • addLPSData(data) - добавляет к свойству data длину прикрепляемых данных (в обратном порядке, как и все числовые значения в протоколе) и сами данные;
  • addULData(data) - прикрепляет "цифровые" данные к пакету в обратном порядке;
  • clear() - сбрасывает свойства data, dlen, msg;
  • __d2s(d) - переводит число d в строку для передачи через сокеты на сервер;
  • __s2d(s) - обратный метода для __d2s(d);

Вот как всё это безобразие выглядит:

""" MRIM constants """
PROTO_VERSION_MAJOR = 1
PROTO_VERSION_MINOR = 12 #7
PROTO_VERSION = (PROTO_VERSION_MAJOR << 16) | PROTO_VERSION_MINOR
CS_MAGIC = 0xDEADBEEF

MRIM_CS_HELLO = 0x1001 # C -> S
MRIM_CS_HELLO_ACK = 0x1002 # S -> C
MRIM_CS_LOGIN_ACK = 0x1004 # S -> C
MRIM_CS_LOGIN_REJ = 0x1005 # S -> C
MRIM_CS_PING = 0x1006 # C -> S
MRIM_CS_MESSAGE = 0x1008 # C -> S
MESSAGE_FLAG_OFFLINE = 0x00000001
MESSAGE_FLAG_NORECV = 0x00000004
MESSAGE_FLAG_AUTHORIZE = 0x00000008 # X-MRIM-Flags: 00000008
MESSAGE_FLAG_SYSTEM = 0x00000040
MESSAGE_FLAG_RTF = 0x00000080
MESSAGE_FLAG_CONTACT = 0x00000200
MESSAGE_FLAG_NOTIFY = 0x00000400
MESSAGE_FLAG_MULTICAST = 0x00001000
MAX_MULTICAST_RECIPIENTS = 50
MESSAGE_USERFLAGS_MASK = 0x000036A8 # Flags that user is allowed to set himself
MRIM_CS_MESSAGE_ACK = 0x1009 # S -> C
MRIM_CS_MESSAGE_RECV = 0x1011 # C -> S
MRIM_CS_MESSAGE_STATUS = 0x1012 # S -> C
MESSAGE_DELIVERED = 0x0000 # Message delivered directly to user
MESSAGE_REJECTED_NOUSER = 0x8001 # Message rejected - no such user
MESSAGE_REJECTED_INTERR = 0x8003 # Internal server error
MESSAGE_REJECTED_LIMIT_EXCEEDED = 0x8004 # Offline messages limit exceeded
MESSAGE_REJECTED_TOO_LARGE = 0x8005 # Message is too large
MESSAGE_REJECTED_DENY_OFFMSG = 0x8006 # User does not accept offline messages
MRIM_CS_USER_STATUS = 0x100F # S -> C
STATUS_OFFLINE = 0x00000000
STATUS_ONLINE = 0x00000001
STATUS_AWAY = 0x00000002
STATUS_UNDETERMINATED = 0x00000003
STATUS_FLAG_INVISIBLE = 0x80000000
MRIM_CS_LOGOUT = 0x1013 # S -> C
LOGOUT_NO_RELOGIN_FLAG = 0x0010 # Logout due to double login
MRIM_CS_CONNECTION_PARAMS = 0x1014 # S -> C
MRIM_CS_USER_INFO = 0x1015 # S -> C
MRIM_CS_ADD_CONTACT = 0x1019 # C -> S
CONTACT_FLAG_REMOVED = 0x00000001
CONTACT_FLAG_GROUP = 0x00000002
CONTACT_FLAG_INVISIBLE = 0x00000004
CONTACT_FLAG_VISIBLE = 0x00000008
CONTACT_FLAG_IGNORE = 0x00000010
CONTACT_FLAG_SHADOW = 0x00000020
MRIM_CS_ADD_CONTACT_ACK = 0x101A # S -> C
CONTACT_OPER_SUCCESS = 0x0000
CONTACT_OPER_ERROR = 0x0001
CONTACT_OPER_INTERR = 0x0002
CONTACT_OPER_NO_SUCH_USER = 0x0003
CONTACT_OPER_INVALID_INFO = 0x0004
CONTACT_OPER_USER_EXISTS = 0x0005
CONTACT_OPER_GROUP_LIMIT = 0x6
MRIM_CS_MODIFY_CONTACT = 0x101B # C -> S
MRIM_CS_MODIFY_CONTACT_ACK = 0x101C # S -> C
MRIM_CS_OFFLINE_MESSAGE_ACK = 0x101D # S -> C
MRIM_CS_DELETE_OFFLINE_MESSAGE = 0x101E # C -> S
MRIM_CS_AUTHORIZE = 0x1020 # C -> S
MRIM_CS_AUTHORIZE_ACK = 0x1021 # S -> C
MRIM_CS_CHANGE_STATUS = 0x1022 # C -> S
MRIM_CS_GET_MPOP_SESSION = 0x1024 # C -> S
MRIM_CS_MPOP_SESSION = 0x1025 # S -> C
MRIM_GET_SESSION_FAIL = 0
MRIM_GET_SESSION_SUCCESS = 1
MRIM_CS_WP_REQUEST = 0x1029 # C->S
PARAMS_NUMBER_LIMIT = 50
PARAM_VALUE_LENGTH_LIMIT = 64
MRIM_CS_ANKETA_INFO = 0x1028 # S->C
MRIM_ANKETA_INFO_STATUS_OK = 1
MRIM_ANKETA_INFO_STATUS_NOUSER = 0
MRIM_ANKETA_INFO_STATUS_DBERR = 2
MRIM_ANKETA_INFO_STATUS_RATELIMERR = 3
MRIM_CS_MAILBOX_STATUS = 0x1033
MRIM_CS_CONTACT_LIST2 = 0x1037 # S->C
GET_CONTACTS_OK = 0x0000
GET_CONTACTS_ERROR = 0x0001
GET_CONTACTS_INTERR = 0x0002
CONTACT_INTFLAG_NOT_AUTHORIZED = 0x0001
MRIM_CS_LOGIN2 = 0x1038 # C -> S
MAX_CLIENT_DESCRIPTION = 256

class packet:
""" MRIM handler """
magic = CS_MAGIC
proto = PROTO_VERSION
seq = 0x00000000
msg = 0x00000000
dlen = 0x00000000
from_addr = 0x00000000
from_port = 0x00000000
reserved = 0x00000000
data = ""
size = 44

def __init__(self):
self.reserved = self.__d2s(0x00000000, 16) # 16 байт

def setHost(self, host, port):
self.from_host, self.from_port = host, port
return True

def getPacket(self, msg):
self.seq += 1
self.msg = msg
self.dlen = len(self.data)
return self.__d2s(self.magic, 4) + self.__d2s(self.proto, 4) + \
self.__d2s(self.seq, 4) + self.__d2s(self.msg, 4) + \
self.__d2s(self.dlen, 4) + self.__d2s(self.from_addr, 4) + \
self.__d2s(self.from_port, 4) + self.reserved + self.data

def setPacket(self, p):
if len(p) < 44:
return False
self.t = p
self.magic = self.__s2d(p[ : 4])
self.proto = self.__s2d(p[4 : 8])
self.seq = self.__s2d(p[8 : 12])
self.msg = self.__s2d(p[12 : 16])
self.dlen = self.__s2d(p[16 : 20])
self.from_addr = self.__s2d(p[20 : 24])
self.from_port = self.__s2d(p[24 : 28])
self.reserved = self.__s2d(p[28 : 44])
if len(p[44 : ]):
self.data = self.__s2d(p[44 : ])
else:
self.data = ""
return True

def addRawData(self, data):
self.data += data
self.dlen = len(self.data)
return True

def addLPSData(self, data):
dlen = len(data)
self.data += self.__d2s(dlen, 4)
self.data += data
self.dlen = len(self.data)
return True

def addULData(self, data):
self.data += self.__d2s(data, 4)
return True

def clear(self):
self.data = ""
self.dlen = ""
self.msg = ""

def __d2s(self, d, size = 4):
d = hex(d)[2 : ]
if d[-1] == "L":
d = d[ : -1]
ln = len(d)
if ln % 2:
ln += 1
d = "0" + d
i = 0
r = ""
while i < ln:
r = chr(int(d[i : i + 2], 16)) + r
i += 2
while len(r) < size:
r += chr(0x00)
return r

def __s2d(self, s):
r = ""
i = len(s)
while i:
t = hex(ord(s[i - 1: i]))[2 : ]
if len(t) < 2: t = "0" + t
r += t
i -= 1
return int(r, 16)

Теперь создадим файл emails.py в котором у нас будут храниться функции, связанные с обработкой аккаунтов. Для хранения информации об аккаунтах я выбрал sqlite. На это меня побудило 2 причины: 1 - это удобно :), 2 - мне надо было разобраться особенностями работы с СУБД из-под python (в частности, надо было разобраться с sqlalchemy).

В emails.py у нас содержится описание таблицы с нашими аккаунтами, в которой будут следующие поля:

  • login - собственно сам email;
  • password - пароль от него;
  • limit - по умолчанию равен 50 - количество sms, которое можно отправить в сутки с аккаунта (mail.ru не позволяет больше);
  • last_sms - время отправки последней sms, mail.ru запрещает слать sms чаще, чем раз в минуту;

А так же следующие функции:

  • add_emails(emails, session) - открывает файл с мыльниками и паролями (разделены пробелами) и добавляет их в нашу базу, если таких аккаунтов там ещё нет, возвращает число добавленных аккаунтов;
  • update_limits(session) - устанавливает лимит sms равный 50 у тех аккаунтов, у которых со времени последней отправки sms прошло больше суток;

Код у меня выглядит так:


import sqlalchemy as sa
from sqlalchemy.orm import mapper, sessionmaker
import time

engine = sa.create_engine("sqlite:///database.db", echo=False)
metadata = sa.MetaData()

emails_table = sa.Table("emails", metadata,
sa.Column("email", sa.String(convert_unicode=False), primary_key=True),
sa.Column("password", sa.String(convert_unicode=False), nullable=False),
sa.Column("limit", sa.Integer, nullable=False, default=50),
sa.Column("last_sms", sa.Integer, nullable=False, default=0)
)

metadata.create_all(engine)

class Email(object):
def __init__(self, email, password, limit = 50, last_sms = 0):
self.email = email
self.password = password
self.limit = limit
self.last_sms = last_sms

def __repr__(self):
return "" % (self.email, self.password, self.limit, self.last_sms)

def add_emails(emails, session):
try:
femails = open(emails, "r")
except:
print "Error. Can't open file %s." % emails
return False
try:
counter = 0
for e in femails:
login, password = e.strip("\r\n").split(" ", 1)
c = session.query(Email).filter(Email.email == login).count()
if not c:
session.add(Email(login, password))
counter += 1
session.commit()
except:
femails.close()
print "Error. Unknow exception :-(."
return False
femails.close()
return counter

def update_limits(session):
e = session.query(Email).filter(Email.last_sms <= int(time.mktime(time.localtime()) - 86400))
for email in e:
email.limit = 50
session.commit()

mapper(Email, emails_table)
Session = sessionmaker(bind = engine)

Теперь пришло время заняться самим клиентом. Создаём очередной файл clnt.py и описываем в нём класс со следующими методами:

  • connect() - создание сокета и подключение к серверу;
  • hello() - отправка MRIM_CS_HELLO и получение в ответ MROM_CS_HELLO_ACK;
  • auth(login, passwd) - отправка запроса на авторизацию;
  • sendSMS(number, text) - отправить sms на номер number. Стоит отметить, что тип пакета с sms - 0x00001039, сам пакет имеет следующий формат - UL 0x00000000 LPS +71234567890, LPS текст sms, где +71234567890 - номер на который мы отправляем сообщение. Так же ещё одна особенность - если среди наших контактов в агенте нет ни одного с указанным номером телефона, то сообщения не будут доходить;
  • send(msg) - пишет msg в уже открытый сокет;
  • get() - читает из сокета и возвращает результат;
  • close() - закрывает сокет
  • start(login, passwd) - вызывает последовательно все необходимые методы для подключения к серверу;
  • __init__(rhost, rport) - конструктор, ему передаётся ip-адрес и порт к которому будет производиться подключение;
# -*- coding: cp1251 -*-

import socket
import os
import urllib
from packets import *

class clnt:
rhost = ""
rport = 0
sock = False
ping = 0
packet = False
rcvd_packet = False

def __init__(self, rhost, rport):
self.rhost, self.rport = rhost, rport
self.packet = p_header()
self.rcvd_packet = p_header()

def connect(self):
# Создаём сокет и подключаемся
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
self.sock.connect((self.rhost, self.rport))
addr, port = self.sock.getsockname()
self.packet.setHost(addr, port)
except:
print "\nError. Connection to server %s:%d failed." % (self.rhost, self.rport)
return False
return True

def start(self, login, passwd):
if not self.connect(): return False
if not self.hello(): return False
if not self.auth(login, passwd): return False

return True

def hello(self):
self.send(self.packet.getPacket(MRIM_CS_HELLO))
#print "Sent MRIM_CS_HELLO."
try:
self.rcvd_packet.setPacket(self.get())
if self.rcvd_packet.msg == MRIM_CS_HELLO_ACK:
#print "Recieved MRIM_CS_HELLO_ACK.",
self.ping = self.rcvd_packet.data
#print "Ping timeout %ds." % self.ping
else:
print "\nError. Not recieved MRIM_CS_HELLO_ACK."
return False
except:
print "\nError. Not recieved MRIM_CS_HELLO_ACK."
return False
return True

def auth(self, login, passwd):
self.packet.clear()
self.packet.addLPSData(login)
self.packet.addLPSData(passwd)
self.packet.addULData(STATUS_ONLINE)
self.packet.addLPSData("STATUS_ONLINE")
self.packet.addLPSData("FreeAgent v 0.1")
print "Trying to authorize as %s..." % login,
try:
self.send(self.packet.getPacket(MRIM_CS_LOGIN2))
except:
print "\nError. Can't send MRIM_CS_LOGIN2."
return False
try:
self.rcvd_packet.setPacket(self.get())
except:
print "\nError. Not recievd answer from server."
return False
if self.rcvd_packet.msg == MRIM_CS_LOGIN_ACK:
print "success!"
return True
else:
print "error :-(."
return False

def sendSMS(self, number, text):
ftext = ""
ftext = text
self.packet.clear()
self.packet.addULData(0x0)
self.packet.addLPSData(number)
self.packet.addLPSData(ftext)
print "Sending to %s: %s..." % (number, text),
try:
self.send(self.packet.getPacket(0x00001039))
except:
print "\nError. Can't send sms"
return False
print "Ok!"
return True

def send(self, msg):
try:
self.sock.send(msg)
except:
print "\nError. Can't send data."
return False
return True

def get(self):
return self.sock.recv(4096)

def close(self):
self.sock.close()
return True

Все основные функции и классы написаны, осталось их только использовать. Создаём главный файл sms.py.
Первое, что мы в нём делаем - это "обновляем" лимиты в 50 сообщений в сутки, далее проверяем количество аргументов командной строки, если оно равно трём, то считаем, что нам переданы номер на который надо отправить sms и текст (т.е. программа была запущена так: sms.py +71234567890 "testovoe soobshenie"), иначе входим в интерактивный режим.
Следующий шаг - пытаемся подключиться к адресу "http://mrim.mail.ru:2042" и узнать адрес сервера к которому нам рекомендуют подключаться на данный момент.
Выбираем из нашей БД список ящиков у которых не превышен лимит, упорядочиваем (по возрастанию) по дате отправки последнего сообщения и отправляем sms с 1го, уменьшая лимит этого ящика на 1. Если со времени последней отправки sms не прошла ещё 61 секунда, то перед отправкой ждём.

# -*- coding: cp1251 -*-

from clnt import *
import sys
import time
import urllib
from emails import *
import os

session = Session()
update_limits(session)

if len(sys.argv) == 3:
number = sys.argv[1]
text = sys.argv[2]
else:
while True:
action = raw_input("Select action:\n1 - add emails from file\n2 - send sms\n")
if action == "1":
ef = raw_input("Enter file name: ")
print "%d new emails added." % add_emails(ef, session)
elif action == "2":
number = raw_input("Enter phone number: ")
text = raw_input("Enter SMS text: ")
break
else:
print "Invalid action!"

try:
saddr = urllib.urlopen("http://mrim.mail.ru:2042").readlines()
saddr = saddr[0]
shost, sport = saddr[:-1].split(":")
sport = int(sport)
except:
print "\nError. Can't connect to mrim.mail.ru."
sys.exit(0)

emails = session.query(Email).filter(Email.limit > 0).order_by(Email.last_sms)
if not emails.count():
print "Sorry, all limits exceeded."
sys.exit(0)
if emails[0].last_sms > (int(time.mktime(time.localtime()) - 61)):
print "Waiting %d seconds." % (61 - (int(time.mktime(time.localtime())) - emails[0].last_sms))
time.sleep(61 - (int(time.mktime(time.localtime())) - emails[0].last_sms))
c = clnt(shost, sport)
if not c.start(str(emails[0].email), str(emails[0].password)):
print "Error."
sys.exit(0)
c.sendSMS(number, text)
emails[0].last_sms = int(time.mktime(time.localtime()))
emails[0].limit -= 1
session.commit()
c.close()
session.commit()

На этом наша программа таки уже закончена. Стоит отметить что не проверяется длина сообщений и отправка кириллицы протестирована только в кодировке CP1251. Все использованные файлы можно скачать из прикреплённого архива.

Ссылки к статье:
http://agent.mail.ru/ru/developers/protocol.html - описание протокола агента.

Пожалуйста, оцените полезность и качество данной статьи. Одна звезда - плохо, 5 - хорошо.
1/5. Мы будем признательны, если вы напишете комментарий с причиной низкой оценки.2/5. Мы будем признательны, если вы напишете комментарий с причиной низкой оценки.3/5. Мы будем признательны, если вы напишете комментарий с причиной низкой оценки.4/5.5/5. (5 голосов, средний: 5,00 из 5)
Загрузка...
  1. topinambur
    16 декабря 2008 в 08:26 | #1

    Спасибо за полезную статью! Пошел переписывать на PHP :)

  2. lizz
    16 декабря 2008 в 12:49 | #2

    Удачи, будем рады, если поедлитесь наработками, может кому ещё пригодятся ;-).

  3. elisk
    23 октября 2013 в 23:11 | #3

    Статья супер!

  4. pethead
    14 апреля 2016 в 10:28 | #4

    ./sms.py
    File "./sms.py", line 24
    print "%d new emails added." % add_emails(ef, session)
    ^
    SyntaxError: invalid syntax

  5. pethead
    14 апреля 2016 в 10:39 | #5

    @ pethead
    Должно быть
    print ("%d new emails added." % add_emails(ef, session))

  6. pethead
    14 апреля 2016 в 11:05 | #6

    raw_input заменен на input

  7. pethead
    14 апреля 2016 в 12:28 | #7

    не хочет

    Error. Can't connect to mrim.mail.ru.

    Хотя в браузере нормально выплевывает адрес.

  8. lizz
    23 мая 2016 в 02:14 | #8

    @pethead

    Про браузер - на какой порт? Проще всего проверить телнетом доступность хоста и порта.

    Про print/raw_input - это вы видимо под python3 пытаетесь запустить?

    Ну и напоследок, рекомендую посмотреть на дату публикации поста. ;) За 8 лет могло что-то поменяться. :))

    P.S. Код, кстати, тут не очень написан. Не уверен даже что я бы рекомендовал на этом примере учиться питону.

  1. Пока что нет уведомлений.