使用 Cloudflare 搭建域名邮箱

Cloudflare 2022年推出了 电子邮件路由 功能,通过这个功能,我们可以直接在 cloudfalre 接收域名邮件,无需再申请其他第三方的域名邮箱服务。

关于该功能的开通已有多篇文章进行介绍,本文就不再赘述。

使用该服务时,可以选择开通 Call all 功能,将所有除自定义地址以外的邮箱,全部进行转发。

转发方式有两种,一种是转发至另一个邮箱,但这种方式有很大弊端。一是该方式仍需使用传统邮箱进行邮件接收,域名邮箱多用于客户服务方面,最好有一种编程的方式可以直接读取邮件。第二点,该方式依托第三方邮箱服务,服务商会对邮箱进行限制,如邮件过滤,登录验证,邮箱容量等,会导致邮件并不能 100% 到达。

第二种方式则使用 Cloudflare Worker 功能,将接收到的电子邮件通过 Worker 处理。结合自己开发的后端服务,将邮件存储到自己的服务器上,这样就可以通过编程的方式来获取邮箱。

Cloudfalre Worker:

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
const { PostalMime } = require('postal-mime');


async function streamToArrayBuffer(stream, streamSize) {
let result = new Uint8Array(streamSize);
let bytesRead = 0;
const reader = stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
result.set(value, bytesRead);
bytesRead += value.length;
}
return result;
}

export default {
async email(message, env, ctx) {
let from = message.headers.get("from");
let to = message.headers.get("to");
let subject = message.headers.get("subject");
let data = "";
if (message.raw != null) {
const rawEmail = await streamToArrayBuffer(message.raw, message.rawSize);
const parser = new PostalMime.default();
const parsedEmail = await parser.parse(rawEmail);
data = parsedEmail.html;
}
let response = await fetch("https://example.com/email", {
method: "POST",
headers: {
"Content-type": "application/json; chatset=UTF-8"
},
body: JSON.stringify({
"from": from,
"to": to,
"subject": subject,
"data": data
})
});
console.log(response.status, response.statusText, await response.text());
}
}

以上 Worker 在部署时,需要使用 Wrangler CLI.

部署完成后,在 call all 中选择该 Worker,域名所收到的邮箱将会全部转发至 https://example.com/email

from, to 不采用 message.from, message.to 是因为在实践中发现, message.from 有事会设置为代发邮箱的邮件地址,无法看到具体发件人的邮件地址

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
func NewEmail(c *fiber.Ctx) error {
email := new(emailBody)
c.BodyParser(email)
email.CreatedAt = int(time.Now().Unix())
//处理 发件人 收件人
reg, _ := regexp.Compile(`<(.*)>`)
fromSubMatch := reg.FindStringSubmatch(email.From)
if len(fromSubMatch) != 0 {
email.From = fromSubMatch[1]
}
toSubMatch := reg.FindStringSubmatch(email.To)
if len(toSubMatch) != 0 {
email.To = toSubMatch[1]
}

domainSplit := strings.Split(email.To, "@")
if len(domainSplit) == 2 {
email.Domain = domainSplit[1]
}
rows, err := db.Query("INSERT INTO `table` (`from`,`to`,`subject`,`data`,`domain`,`created_at`) VALUES(?,?,?,?,?,?)", email.From, email.To, email.Subject, email.Data, email.Domain, email.CreatedAt)
if err != nil {
log.Println(err)
return c.SendString("ERROR")
}
defer rows.Close()
return c.SendString("OK")
}

在该代码中,会将转发过来的邮件存储到数据库中,以便日后查询。需要注意的是 from, to 参数均进行正则过滤。是因为 from, to 参数可能含有 发件人,收件人自定义名称,需要进行删除。

对于发件服务,我目前使用 AWS SES 进行发件。该服务可以直接仅配置子域的 mx 记录,完成域名的认证。然后就可以自定义发件人名称向不特定邮箱进行发件。

求解最大子数组

暴力破解法

通过遍历每个子数组,找到最小的子数组

算法复杂度 $$n^2$$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func FindMaxSubArr(A []int) (int, int, int) {
var max = A[0]
var current = 0
var left, right = 0, 0
var LengthA = len(A)
for i := 0; i < LengthA; i++ {
current = A[i]
if current > max { // 判断初始的单元是否是最大数组,第一次写的时候忘记加了,导致有些排列会出问题
max = current
left, right = i, i
}
for k := i + 1; k < LengthA; k++ {
current += A[k]
if current > max {
max = current
left, right = i, k
}
}
}

return left, right, max
}

分治法

分解数组规模,递归的求解左右子数组及跨越中间的数组。
将数组分为两部分,最大子数组要么在左边的子数组,要么在右边的子数组,要么跨越两个子数组

算法复杂度 $$n log(n)$$

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
func FindMaxSubArr(A []int) (int, int, int) {
if len(A) == 1 {
return 0, 0, A[0]
}
middle := len(A) / 2
ll, lr, lm := FindMaxSubArr(A[:middle])
rl, rr, rm := FindMaxSubArr(A[middle:])
ml, mr, mm := FindCrossMiddleSubArr(A[:])

if lm >= rm && lm >= mm {
return ll, lr, lm
} else if rm >= lm && rm >= mm {
return rl + middle, rr + middle, rm
} else {
return ml, mr, mm
}
}

func FindCrossMiddleSubArr(A []int) (int, int, int) {
lens := len(A)
if lens == 1 {
return 0, 0, A[0]
}
middle := lens / 2
left, right := middle-1, middle
lmax, rmax := A[left], A[right]
ltemp := lmax
rtemp := rmax
i := left
k := right
for i > 0 {
i--
ltemp += A[i]
if lmax < ltemp {
lmax = ltemp
left = i
}
}

for k < lens-1 {
k++
rtemp += A[k]
if rmax < rtemp {
rmax = rtemp
right = k
}
}

return left, right, lmax + rmax
}

动态规划

若已知 A[1.. j] 的最 大子数组,基于如下性质将解扩展为 A[1. .j+1] 的最大子数组: A[1. .j+1] 的最大子数组要么是 A[1.. j] 的最大子数组,要么是某个子数组 A[i.. j+1])
简单的理解,给一个已知最大子数组的数组增加一个单元,新数组的最大子数组如果产生了变化,会有两种情况:

  1. 最大子数组不变
  2. 最大子数组内包含新增的单元

初版错误的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func FindMaxSubArr(A []int) (int, int, int) {
max := A[0]
left, right := 0, 0

leftToNow := max
tempLeft := left
for i := 1; i < len(A); i++ {
leftToNow = leftToNow + A[i]
if leftToNow > max && leftToNow > A[i] {
max = leftToNow
left = tempLeft
right = i
} else if A[i] > leftToNow && A[i] > max {
max = A[i]
leftToNow = max
left, right = i, i
tempLeft = left
}
}

return left, right, max
}

随机测试发现 类似序列 [-8 -4 6 -8 5 7 -5 6 -1 -7] 并不能正常生成最大子数组。
问题在于左界只会因为之前的子数组小于当前单元时会右移重置,会导致计算的最大子数组中包含了左侧连续的和为负的子数组,导致整体的和变小。
解决这个问题,我们需要知道。 x < 0, x + P <= P
我们需要动态的舍去已经计算出的负数子数组。当前子数组的和已经小于零,那么不论下一个单元是正是负,都会大于或者等于当前的子数组的合。

1
2
3
4
 else if leftToNow < 0 {
leftToNow = 0
tempLeft = i + 1
}

再次测试的时候,发现还是有问题,问题出在这两行

1
2
3
if leftToNow >= max && leftToNow > A[i]

} else if A[i] > leftToNow && A[i] > max {

我们仅当 leftNow > A[i] 时才更新 max, 但是通过上面的代码,我们可能已经将leftToNow 更新成了 A[i] , 因此它将永远不会成立。
我们只需要将两个条件中的任意一个 leftToNow 与 A[i] 的比较进行 == 的判断就可以了

算法复杂度 $$n$$

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
func FindMaxSubArr(A []int) (int, int, int) {
max := A[0]
left, right := 0, 0

leftToNow := max
tempLeft := left
for i := 1; i < len(A); i++ {
leftToNow = leftToNow + A[i]
if leftToNow > max && leftToNow >= A[i] {
max = leftToNow
left = tempLeft
right = i
} else if A[i] > leftToNow && A[i] > max {
max = A[i]
leftToNow = max
left, right = i, i
tempLeft = left
} else if leftToNow < 0 {
leftToNow = 0 // 循环开始时会被初始化为 A[i+1] 的值,不会产生负面影响
tempLeft = i + 1
}
}

return left, right, max
}

实际上 如果采用

1
} else if A[i] >= leftToNow && A[i] > max {

tempLeft = i + 1 也是可以省略的

Laravel 扩展 - Seeder 填充数据生成

Laravel 框架支持 db:seed 来填充表。相对应的,需要自己编写 DatabaseSeeder.php 文件。 如果填充文件行数少,或者可以利用工厂类生成会很方便。
但是有些数据是需要手动填写与组合的。例如:目录的组织,权限的组织,通用配置的组织。数据大,改动多,不易组织。

nuyfeng/make-database 可以直接通过数据库来生成 Seeder 文件,省去的繁琐的复制。

GITHUB: make-database

1
2
3
4
5
6
7
8
9
10
11
12
13
composer require --dev nuyfeng/make-database   //安装扩展

php artisan database:build-seeder --help

Description:
根据数据库生成 seeder 数据

Usage:
database:build-seeder [options]

Options:
--max-rows[=MAX-ROWS] 表允许最大的行数,超过此值跳过生成. 0为不限制 [default: "100"]
--tables[=TABLES] 指定生成数据表表列表 example users,sites,logs

Windows Docker Proxy

Dockerhub 在中国的访问速度不佳,我们通常需要使用代理来进行加速。
Windows Docker 是基于 WSL 运行的,并不是直接运行在我们的 Windows 系统之上。
通常我们设置代理会使用 127.0.0.1 作为代理的主机地址,但是在 WSL 中 127.0.0.1 已经成为了 WSL Linux 的回环地址,无法链接到运行在 Windows 上的代理服务。
安装 WSL 后,系统会生成一个虚拟的网卡:
打开 PowerShell (cmd), 输入 ipconfig

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
PS C:\Users\program-03> ipconfig

Windows IP 配置


以太网适配器 vEthernet (WSL):

连接特定的 DNS 后缀 . . . . . . . :
本地链接 IPv6 地址. . . . . . . . : fe80::a179:32ed:cebd:24df%28
IPv4 地址 . . . . . . . . . . . . : 172.22.144.1
子网掩码 . . . . . . . . . . . . : 255.255.240.0
默认网关. . . . . . . . . . . . . :

以太网适配器 以太网:

连接特定的 DNS 后缀 . . . . . . . :
本地链接 IPv6 地址. . . . . . . . : fe80::4ca0:44b2:46f4:25f6%5
IPv4 地址 . . . . . . . . . . . . : 192.168.6.103
子网掩码 . . . . . . . . . . . . : 255.255.255.0
默认网关. . . . . . . . . . . . . : 192.168.6.1

以太网适配器 VMware Network Adapter VMnet1:

连接特定的 DNS 后缀 . . . . . . . :
本地链接 IPv6 地址. . . . . . . . : fe80::ad37:b9f8:8199:dc60%8
IPv4 地址 . . . . . . . . . . . . : 192.168.111.1
子网掩码 . . . . . . . . . . . . : 255.255.255.0
默认网关. . . . . . . . . . . . . :

以太网适配器 VMware Network Adapter VMnet8:

连接特定的 DNS 后缀 . . . . . . . :
本地链接 IPv6 地址. . . . . . . . : fe80::e09c:ce9c:952b:4494%6
IPv4 地址 . . . . . . . . . . . . : 192.168.40.1
子网掩码 . . . . . . . . . . . . : 255.255.255.0
默认网关. . . . . . . . . . . . . :

vEthernet (WSL) 就是 WSL 的默认地址,开启代理的局域网访问功能。
设置 Docker Proxy 为 http://172.22.144.1:10811

DcatAdmin 表头固定

DcatAdmin 默认不提供固定表头的功能,可以通过自定义 CSS 来实现
本 CSS 来自 DcatAdmin 项目中另一个地方的样式。

将代码直接贴在需要固定表头的 Grid 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Admin::style(<<<STYLE
thead tr:nth-child(1) th {
background-color: #fff;
position: sticky;
top: 60px; // 顶部 Banner
z-index: 10;
}
thead tr:nth-child(2) th {
background-color: #fff;
border-bottom: 0;
position: sticky;
top: 101px; //手动计算的
z-index: 10;
}
STYLE
);

其中 top 是我手动计算的,可能要根据实际高度自行修改。或者有自适应的方法也欢迎提供。
效果如题所示:

Snipaste_2022-03-31_14-22-34.png

VMwaer 克隆后IP重复

VMware 克隆后,不会给机器分配新的IP,造成多个机器共用处理的IP。需要手动配置IP:

系统版本 : Ubuntu 20.4

编辑网络配置信息:

1
2
cd /etc/netplan
sudo vi 00-installer-config.yaml

需要注意的是,配置文件名并不一定是一样的,需要替换成自己的。

编辑以下内容:

1
2
3
4
5
6
7
8
9
10
11
# This is the network config written by 'subiquity'
network:
ethernets:
ens33:
dhcp4: false
addresses: [192.168.111.10/24]
gateway4: 192.168.111.2
nameservers:
addresses: [192.168.111.2]
search: []
version: 2

我的虚拟机用的是 NAT 模式,以上内容可以根据自己的配置替换。详细信息在 “导航栏->编辑->虚拟网络编辑器”

重启生效:

1
sudo netplan apply

Laravel POST 参数过多引发的 405错误

Laravel 中,更新一个资源应该使用 PUT 方法。但是在实现中, FORM 表单只支持 POST,GET 方法,在其中添加 _method 字段让 Laravel 将请求当作 PUT,DELETE等字段。


今天正常运行的系统突然无法更新数据,HTTP STATUS 405 提示信息为:

1
The POST method is not supported for this route. Supported methods: GET,HEAD,PUT,PATCH,DELETE.

由于这个错误提示的误导,我一直以为是路由出了问题,往这方面排查了挺长时间。
最终通过查看站点日志,发现 PHP 报错了:

1
FastCGI sent in stderr: "PHP message: PHP Warning:  Unknown: Input variables exceeded 1000. To increase the limit change max_input_vars in php.ini. in Unknown on line 0" while reading response header from upstream

到此问题就很清楚了,报错的表单中有很多 checkbox 导致处于表单末尾的 _method 因为默认最大提交限制为 1000 的问题无法被系统接收到,从而导致系统无法根据 _method 项来用 PUT 方式处理 POST 请求。

处理方法也很简单,在 php.ini 中修改 max_input_vars 的值即可。记得要去掉配置项前面的 ; 符号,不然不会生效(我第一次更改后还是报错,以为不是这里的问题,检查后才发现值虽然改了,但是默认是注释状态)。

PHP8 - Named arguments 与 Constructor property promotion

PHP8 已于 2020-11-26 发布,这次带来了我们期待已久的 JIT 。但这篇文章主要讲讲新特性的理解与使用。

Named arguments

我们会在函数参数列表中使用默认值:

1
2
3
function setUser(int $age, string $name, string $money = "0.00", int $sex = 0, $job = null)
{
}

以前当我们想要使用 $money 的默认值并设置 $sex 的值时:

1
setUser(12,"Lilin", "0.00", 1);

$money 的值被我们显式的设置了,似乎使函数声明中的默认值“浪费了”
在 PHP8 中,我们可以根据参数名来设置参数的值:

1
setUser(12,name:"Lilin", sex:1);

我们可以通过指定参数名来设置对应的参数值,这么做的好处不仅仅可以使我们更方便的使用预设的默认值.代码不是一成不变的,在某一天我们需要维护上个月的代码,如果我遇到了这样的函数调用:

1
getUserWith($userId, true, 5, 100, today(), true)

我想我的内心一定是崩溃的,我可以根据经验推断出 $userId 是用户 ID , today() 是根据今天的日期搜索。但是有两个 true , 这似乎需要阅读参数列表才能判断出来。
如果使用命名参数,情况就会变成这样:

1
getUserWith(userId:$userId, emailVerify:true, limit:5, offset:100, time:today(), telphoneVerify:true)

Constructor property promotion

以前我们在对象初始化时,往往这样操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Point {
public float $x;
public float $y;
public float $z;
public function __construct(
float $x = 0.0,
float $y = 0.0,
float $z = 0.0
) {
$this->x = $x;
$this->y = $y;
$this->z = $z;
}
}

现在我们可以在构造函数中完成这一切:

1
2
3
4
5
6
7
class Point {
public function __construct(
public float $x = 0.0,
public float $y = 0.0,
public float $z = 0.0,
) {}
}

与 Named arguments 配合,我们的函数初始化将变得十分简洁:
new Point(z:1.0)


PHP8官网介绍页

PHP RFC: Named Arguments

CodeIgniter4 日期自动更新 useTimestamps

在 CodeIgniter4 中,如果想要 Model 在创建及更新数据时自动更新对应的日期字段,可以使用 useTimestamps。但是有很多人会遇到遇到一些问题,其中最主要的问题是数据库字段与数据内容不匹配导致的。我们在设置了 $useTimestamps = true 后,往往忽略了与之配套使用的字段 dateFormat

一般的,我们在数据库中存储时间使用时间戳,我一般设置为 int UNSIGNED。大多人也是如此,在 CI4 中 dateFormat 的默认值为 datetime

1
2
3
4
5
6
7
8
9
case datetime:
$converted = $value->format(Y-m-d H:i:s);
break;
case date:
$converted = $value->format(Y-m-d);
break;
case int:
$converted = $value->getTimestamp();
break;

默认的日期格式为 2020-05-09 12:12:12 如果我们的数据会为 int 则数据会存储被为 2020 ,后边的信息无法存储。
所以我们需要将日期格式设置为 $dateFormat = int

CodeIgniter4 useSoftDeletes 软删除

做数据删除的时候,我们往往会用到软删除。CI4对这一功能进行了支持。

CI4 使用 deleted_at 字段是否为 NULL 来判断是数据是否被删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php namespace App\Models;

use CodeIgniter\Model;

class UserModel extends Model
{
protected $table = users;
protected $primaryKey = id;

protected $returnType = array;
protected $useSoftDeletes = true;

protected $allowedFields = [name, email];

protected $useTimestamps = false;
protected $createdField = created_at;
protected $updatedField = updated_at;
protected $deletedField = deleted_at;

protected $validationRules = [];
protected $validationMessages = [];
protected $skipValidation = false;
}

如果 useSoftDeletes 设置为 true, 则所有数据结果默认排除掉已经删除的数据,具体的做法是会在 SQL 语句中增加 deleted_at IS NULL 来进行数据的过滤。

有很多人在 GITHUB 发 ISSUES 表示自己的 useSoftDeletes 无法正常工作,实际上是因为我们默认习惯将 deleted_at 默认设置为 int, DEFAILT 0, NOT NULL。我们需要让 deleted_at 允许 NULL 并且默认为 NULL 。这样我们才能使用 useSoftDelets

如果我们希望返回的数据临时包括已删除的数据, 我们可以使用 withDeleted

如果我们希望返回的数据只包括已删除的数据,我们可以使用 onlyDeleted