# 在线API
# 一. 交付方式
邮箱画像部署在云端,通过HTTPS的RESTful接口查询使用。每个注册客户会分配一个snuser和snkey。(在控制台-配置 (opens new window)页面可以查看)
邮箱画像服务端对查询请求实行IP白名单机制,只有白名单中IP地址允许查询。支持添加IP段“127.0.0.1/16 ”。(配置管理地址控制台-配置 (opens new window))
# 二. 接口说明
# 2.1请求详情
# 2.1.1 请求参数说明
| 请求参数 | 说明 |
|---|---|
| 请求方法 | POST |
| 请求地址 | https://black-mailbox.yazx.com/v1/api/check/full/mailbox/sha1 |
| 并发 | 200 |
| QPS | 1000 |
| 延时情况 | 100ms以内 |
# 2.1.2 请求说明
请求头
| 参数名称 | 参数类型 | 是否必传 | 参数说明 |
|---|---|---|---|
| x-snuser | string | 是 | 用户识别id(配置管理页面中可以查看) |
请求体
| 传参方式 | 参数类型 | 是否必传 | 参数说明 |
|---|---|---|---|
| 请求体 | string | 是 | aes加密data后的请求数据内容,直接传入请求体 |
data内容如下
| 参数名称 | 参数类型 | 是否必传 | 参数说明 |
|---|---|---|---|
| email_prefix_sha1 | string | 是 | 邮箱前缀(sha1加密) 示例:hunter@yazx.com,取hunter的sha1 结果: 6e2f9e6111e77edd0c446ea7a84e25323d137a61 |
| email_suffix | string | 是 | 邮箱后缀 示例:hunter@yazx.com,取yazx.com |
# 2.2 返回详情
# 2.2.1 返回参数说明
| 参数名称 | 参数类型 | 是否必传 | 参数说明 |
|---|---|---|---|
| status | int | 是 | 状态码 |
| data | json_object | 是 | 威胁猎人识别的数据内容 |
| errmsg | string | 是 | 错误详情 |
data内容如下
| 参数名称 | 参数类型 | 是否必传 | 参数说明 |
|---|---|---|---|
| email_prefix_sha1 | string | 否 | 查询邮箱的前缀(sha1加密) |
| email_suffix | string | 是 | 查询邮箱的后缀 |
| type | integer | 是 | 邮箱类型 0 未知邮箱:未能识别该邮箱类型 1 公共邮箱:指在邮箱门户网站注册的邮箱,使用者多为正常用户。 2 临时邮箱:指无需注册登录且只有几分钟到几小时不等有效性即可收发邮件的邮箱,使用者多为恶意用户。 3 企业邮箱:指通过企业认证的邮箱 ,使用者为企业在职员工。 4 校园邮箱:指通过校园认证的邮箱 ,使用者为学校的校园师生。 5 无效邮箱:指不能收发邮件的邮箱。 6 自建邮箱:指私人搭建邮件服务器,所有者可以随意增加或更换邮箱账号。 |
| ban | integer | 是 | 0 未命中邮箱黑名单库 1 命中邮箱黑名单库 |
| risk | integer | 是 | 0 无风险,该邮箱未命中邮箱黑名单库且邮箱类型非临时邮箱 1 有风险,该邮箱命中邮箱黑名单库或邮箱类型为临时邮箱 |
| type_update_time | datetime | 是 | 该邮箱类型最后一次更新的时间 示例:2018-01-06T12:21:50+00:00 |
| ban_update_time | datetime | 是 | 该邮箱黑名单最后一次更新的时间 示例:2018-01-06T12:21:50+00:00 |
# 2.3 错误码
2.3.1当响应的status_code为422时,响应体格式如下:
| 参数名称 | 参数类型 | 是否必返 | 参数说明 |
|---|---|---|---|
| status | int | 是 | 状态码 |
| errmsg | string | 是 | 错误描述 |
| detail | String | 是 | 错误详情 |
status和errmsg示例如下:
| status | errmsg |
|---|---|
| 10001 | bad_data |
2.3.2当响应的status_code为400时,响应体格式如下:
| 参数名称 | 参数类型 | 是否必返 | 参数说明 |
|---|---|---|---|
| status | int | 是 | 状态码 |
| errmsg | string | 是 | 错误描述 |
status和errmsg示例如下:
| status | errmsg |
|---|---|
| 11000 | X-Snuser header is empty |
| 11001 | unknown value in X-Snuser header |
| 11002 | Service not valid |
| 11003 | request not allow to access from this client by service(请添加ip白名单) |
2.3.3当响应的status_code为404时,响应体格式如下:
| 参数名称 | 参数类型 | 是否必返 | 参数说明 |
|---|---|---|---|
| status | int | 是 | 状态码 |
| errmsg | string | 是 | 错误描述 |
status和errmsg示例如下:
| status | errmsg |
|---|---|
| 9999 | Not Found |
# 三. 使用建议
3.1 临时邮箱:type返回值为“2”
由于临时邮箱具有一次性、用完即毁的特点,决定了使用该类邮箱的用户绝非对业务有价值的用户。 同时由于临时邮箱的创建成本极其低廉,黑灰产很容易就能批量创建大量临时邮箱账号从而进行垃圾注册 。
3.2 命中邮箱黑名单:ban返回“1”
该类邮箱是我们明确发现在被黑产持有,而非被自然人所持有的电子邮箱,产生的误判概率极低。
综上所述,对risk返回值为“1”的用户可以考虑在单一策略下直接拦截。对risk返回值为“0”的用户可以使用邮箱类型补充用户的画像。
# 四. 代码示例
# python
# -*- encoding: utf-8 -*-
"""
python 版本
# 3.7及以上
# 依赖包
pip install pycryptodome
pip install requests
"""
import requests
import json
from Crypto.Cipher import AES
from Crypto import Random
SNUSER: str = "test"
SNKEY: str = "test"
def aes_encrypt_seg(content: bytes) -> bytes:
remainder: int = len(content) % AES.block_size
padded_value: bytes
if remainder:
padded_value = content + b'' * (AES.block_size - remainder)
else:
padded_value = content
# 随机 16 字节的密钥
iv: bytes = Random.new().read(AES.block_size)
# CFB 模式
cipher = AES.new(bytes(SNKEY, encoding='utf8'), AES.MODE_CFB, iv, segment_size=128)
value: bytes = cipher.encrypt(padded_value[:len(content)])
ciphertext: bytes = iv + value
return ciphertext
def check_full_mailbox_sha1():
POST_URL: str = "https://black-mailbox.yazx.com/v1/api/check/full/mailbox/sha1"
# 通过请求头告诉我们snuser
headers: dict = {'X-Snuser': SNUSER, "content-type": "text/plain"}
# 通过snkey加密请求体
payload: dict = {
"email_prefix_sha1": "6058a57e3783ec526dbabc461bdc9bce718385bb",
"email_suffix": "truthfinderlogin.com",
}
data: bytes = aes_encrypt_seg(json.dumps(payload).encode())
r = requests.post(url=POST_URL, headers=headers, data=data)
rjson: dict = r.json()
if rjson["status"] != 200:
print(rjson['errmsg'])
else:
print(rjson['data'])
if __name__ == "__main__":
check_full_mailbox_sha1()
# java
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import okhttp3.*;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.stream.Collectors;
public class EmailMain {
private static String SNUSER = "****";
private static String SNKEY = "****";
private static final String URL = "https://black-mailbox.yazx.com/v1/api/check/full/mailbox/sha1";
private static byte[] encryptAES(String data) {
try {
SecretKeySpec key = new SecretKeySpec(SNKEY.getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES/CFB/NoPadding");
byte[] databyteContent = data.getBytes();
byte[] ivbytes = new byte[16];
Random rand = new Random();
rand.nextBytes(ivbytes);
IvParameterSpec ivSpec = new IvParameterSpec(ivbytes);
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
byte[] result = cipher.doFinal(databyteContent);
byte[] finalbytes = new byte[ivbytes.length + result.length];
System.arraycopy(ivbytes, 0, finalbytes, 0, ivbytes.length);
System.arraycopy(result, 0, finalbytes, ivbytes.length, result.length);
return finalbytes;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private static void checkEmail(Map<String, Object> payload) throws IOException {
JSONObject jsObj = new JSONObject(payload);
System.out.println("request plain =>" + jsObj);
byte[] content = encryptAES(jsObj.toString());
httpPostText(content);
OkHttpClient client = new OkHttpClient();
MediaType mediaType = MediaType.parse("text/plain");
System.out.println("request:"+ new String(content));
RequestBody body = RequestBody.create(mediaType, content);
Request request = new Request.Builder()
.url(URL)
.post(body)
.addHeader("X-Snuser", SNUSER)
.build();
try (Response response = client.newCall(request).execute()) {
String jsonData = response.body().string();
System.out.println("response plain =>" + jsonData);
JSONObject object = JSON.parseObject(jsonData);
String encryptData = object.getString("data");
System.out.println("result => success: " + encryptData);
} catch (Exception e) {
System.out.println("result => failed");
e.printStackTrace();
}
}
public static void httpPostText(byte[] payload) throws IOException {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost(URL);
httpPost.setHeader("Content-Type", "text/plain");
httpPost.setHeader("X-Snuser", SNUSER);
ByteArrayEntity entity1 = new ByteArrayEntity(payload);
entity1.setContentType("text/plain");
httpPost.setEntity(entity1);
CloseableHttpResponse execute = httpClient.execute(httpPost);
if (execute != null && execute.getStatusLine().getStatusCode() == 200) {
HttpEntity entity = execute.getEntity();
String result = new BufferedReader(new InputStreamReader(entity.getContent())).lines().collect(Collectors.joining());
System.out.println("result => success:" + result);
} else {
System.out.println("result => faild:" + execute.getStatusLine());
}
}
public static void main(String[] args) throws IOException {
HashMap<String, Object> map = new HashMap<>();
map.put("email_suffix", "truthfinderlogin.com");
map.put("email_prefix_sha1", "6058a57e3783ec526dbabc461bdc9bce718385bb");
checkEmail(map);
}
}