В mail.ru агенте есть возможно одна полезная вещь — отправка до 50 sms в сутки с одного аккаунта. Протокол агента является открытым и его можно посмотреть на сайте, однако выложена не совсем свежая версия и не сказано ни слова об отправке sms, но это не проблема.
И так, смотрим описание протокола. Для начала нам надо подключится к mrim.mail.ru на порт 2042 или 443 и адрес сервера агента и порт в обычном текстовом формате «ip:port». Затем надо отправить пакет MRIM_CS_HELLO. Как сказано в описании, заголовок пакета имеет следующий формат:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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);
Вот как всё это безобразие выглядит:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 | """ 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 прошло больше суток;
Код у меня выглядит так:
i
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | mport 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-адрес и порт к которому будет производиться подключение;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | # -*- 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 секунда, то перед отправкой ждём.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | # -*- 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
Спасибо за полезную статью! Пошел переписывать на PHP

Статья супер!
./sms.py
File «./sms.py», line 24
print «%d new emails added.» % add_emails(ef, session)
^
SyntaxError: invalid syntax
Должно быть
print («%d new emails added.» % add_emails(ef, session))
raw_input заменен на inpu
не хочет
Error. Can’t connect to mrim.mail.ru.
Хотя в браузере нормально выплевывает адрес