用Python向Kindle推送电子书

使用Kindle的同学应该知道Amazon官方有个Send To Kindle的插件可以方便的把文档推送到你的设备上,可惜的是,这个插件只能用美亚的账号登陆,何不自己做一个?

我从昨天睡觉之前萌生了这个想法,到现在做出来原型整整用了一天,惭愧不已,看来还需多加练习。

简单说下原理

完整的源码我会放在最后,具体的细节实现可以看源码。这里我就简单的说下我的思路以及过程中遇到的值得记录一下的”难点“。

过程其中很简单,就是用SMTP协议把文档以邮件的形式发送给Kindle邮箱。那么,我们需要做的其实只有:

  • 一个UI用来收集Kindle邮箱、推送邮箱、推送邮箱密码和要推送的文档
  • 使用SMTP协议

邮件服务商

SMTP协议是应用层的网络协议,由TCP协议支持,换句话说,它是在保证了可靠传输的基础上通过一定的”暗号“交接来传递邮件,过程大概是:打招呼(hello),确认交流方式(加密吗?用什么加密协议?),身份认证(采用base64编码的用户名和密码),传递内容,结束。具体的指令可能会因为邮件服务商所用的加密协议有所不同,但过程基本就是这样。这样的文章网上一搜一大堆:SMTP协议--在cmd下利用命令行发送邮件,有兴趣的可以自己打一打指令,对理解协议有好处。

但有一点我想说的是这些文章中使用的都是端口25(SMTP的默认端口,传输不加密),而经过我的测试,QQ、163甚至Gmail都已经不再开放25号端口了,用的最多的是TLS和SSL的端口进行加密传输。在SMTP交互过程中,对应的指令变化就是在auth login之前要先发送starttls指令,同时在连接服务器的时候使用端口587。

这里我要吐槽一下QQ和163邮箱所谓的授权码,我不知道是出于什么原因要让第三方邮件客户端使用这个授权码。它真的让你的邮箱更安全了吗?在使用SMTP协议时,QQ和163在验证身份时都要求提供采用base64编码的授权码。Hmm...

汉字转拼音库

在使用smtplib库时,我发现当附件名是中文的时候,收件方收到的邮件中附件会变成一个bin文件。在尝试调整编码无果之后,我想到了一个把中文都转化为拼音的方案。接着去搜了一下还真有一个第三方库:Pinyin

安装这个库的方法是,将整个github库下载到本地,解压缩,用cmd切换到有setup.py那个目录,然后执行:python setup.py install

其他

除了上述两点,本项目中还有几个值得一提的问题或者说我学到的新知识:

  • Python中将键盘鼠标的操作和函数绑定:Events and Bindings,它可以用来实现一个带有超链接效果的Label。
  • 使用文件对话框选取一个文件
  • 怎样清空一个Entry插件

都在代码里了。

目前使用的一些约束

因为只是一个初版,难免有些考虑不到的地方或者说bug,考虑到这只是个练手的项目,我应该也不会继续完善它了,目前我能想到的一些约束包括:

  • 只支持三个邮件服务商:QQ、163和Gmail。目前会对用户输入的推送邮箱和密码做一些简单的校验,但是仅限于判断其是不是以qq.com, 163.comgmail.com结尾,用例如13@3gmail.com就是校验不出来的。kindle邮箱也有类似的问题。同时如果邮箱或密码错误,也不能返回相应的错误消息。
  • 推送的文件大小不能超过50MB,这个其实不是bug,超过50MB的文件即使推送了也会被Amazon退回。同样文件的类型我也不能控制,如果用户选择了kindle不支持的文件类型,软件仍然会推送,只不过同样会被Amazon退回。
  • 在正常流程下,点击发送按钮之后程序会”停滞“很长时间,多长取决于文件的大小。考虑应该添加一个类似于进度条的东西用来缓解用户等待时产生的”焦虑感“。
  • 目前一次只能推送一个文件。考虑将”选择文件“那个Frame做成一个可以进行CRUD操作的LIstbox,但要校验总文件大小不能超过50MB。
  • 还有一些诸如邮箱必须开启SMTP服务,推送邮箱必须处于Amazon账户信任列表里的条件不属于本软件范畴,但是确实必要的,我就不一一列出来的。总之,如果你手动发邮件能成功推送文档,那么软件也可以, 亲测有效。

完整代码

效果图

69520-61d7b2abbb4dab81.jpg
SentToKindle效果图.jpg

smtp.py

import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header
 
def SendToKindle(mail_host, mail_user, mail_pass, receiver, fullpath, bookname):
    message = MIMEMultipart()
    message['From'] = Header("SentToKindle", 'utf-8')
    message['To'] =  receiver 
    message['Subject'] = Header('convert')

    att = MIMEText(open(fullpath, 'rb').read(), 'base64', 'utf-8')
    att["Content-Type"] = 'application/octet-stream'
    att["Content-Disposition"] = 'attachment; filename=%s' % bookname
    message.attach(att)

    smtpObj = smtplib.SMTP(mail_host, 587)
    smtpObj.ehlo()
    smtpObj.starttls()
    smtpObj.login(mail_user, mail_pass)
    smtpObj.sendmail(mail_user, [receiver], message.as_string())
    smtpObj.quit()

ui.py

import tkinter as tk
import tkinter.messagebox
import tkinter.filedialog
from tkinter import END
import webbrowser
import os.path

import smtp
import pinyin

mail_host = ''
mail_user = ''
mail_pass = ''
receiver  = ''
fullpath  = ''
bookname  = ''

class SentToKindleUI(object):
    def __init__(self, object):
        # 推送信息
        self.lf_sendinfo = tk.LabelFrame(object, width=256, height=144, text='推送信息')  
        self.lf_sendinfo.grid(row=0, column=0, sticky='w',padx=10)
        
        self.label_sendinfo_kindlemail = tk.Label(self.lf_sendinfo, width=12, text='Kindle邮箱:')
        self.label_sendinfo_kindlemail.place(x=5,y=2)
        self.label_sendinfo_entry1 = tk.Entry(self.lf_sendinfo, relief='solid')
        self.label_sendinfo_entry1.place(x=100, y=2)

        self.label_sendinfo_sendmail = tk.Label(self.lf_sendinfo, width=12, text='推送邮箱:')
        self.label_sendinfo_sendmail.place(x=5,y=30)
        self.label_sendinfo_entry2 = tk.Entry(self.lf_sendinfo, relief='solid')
        self.label_sendinfo_entry2.place(x=100, y=30)

        self.label_sendinfo_password = tk.Label(self.lf_sendinfo, width=12, text='推送邮箱密码:')
        self.label_sendinfo_password.place(x=5,y=58)
        self.label_sendinfo_entry3 = tk.Entry(self.lf_sendinfo, relief='solid',show='*')
        self.label_sendinfo_entry3.place(x=100, y=58)

        # 校验三个Entries的内容
        def label_sendinfo_bt_click():
            global mail_host, receiver, mail_user, mail_pass
            receiver  = self.label_sendinfo_entry1.get()
            mail_user = self.label_sendinfo_entry2.get()
            mail_pass = self.label_sendinfo_entry3.get()

            # 检查kindle邮箱
            if receiver.endswith('kindle.com') or receiver.endswith('kindle.cn'):
                pass
            else:
                tk.messagebox.showinfo(title='HI', message='Kindle邮箱必须以kindle.com或kindle.cn结尾。')
                self.label_sendinfo_entry1.delete(0, END)
                return

            # 检查推送邮箱    
            if mail_user.endswith('gmail.com'):
                mail_host = 'smtp.gmail.com'
            elif mail_user.endswith('163.com'):
                mail_host = 'smtp.163.com'
            elif mail_user.endswith('qq.com'):
                mail_host = 'smtp.qq.com'
            else:
                tk.messagebox.showinfo(title='HI', message='目前仅支持QQ、163和Gmail邮箱作为推送邮箱。')
                self.label_sendinfo_entry2.delete(0, END)
                self.label_sendinfo_entry3.delete(0, END)
                return

            # 如果能进行到这,说明内容校验都没问题
            tk.messagebox.showinfo(title='HI', message='输入没有问题!')


        varCheck = tk.IntVar()

        def label_sendinfo_checkbutton_click():
            if varCheck.get() == 1:
                self.label_sendinfo_entry3.config(show='')
            else:
                self.label_sendinfo_entry3.config(show='*')

        self.label_sendinfo_checkbutton = tk.Checkbutton(self.lf_sendinfo,
            text = '显示密码',
            variable = varCheck,
            onvalue = 1,
            offvalue = 0,
            command = label_sendinfo_checkbutton_click
            )
        self.label_sendinfo_checkbutton.place(x=90,y=86)

        self.label_sendinfo_bt = tk.Button(self.lf_sendinfo,
            text='校验',
            width=8,
            command=label_sendinfo_bt_click
            )
        self.label_sendinfo_bt.place(x=175,y=86)

        # 文件选择
        self.lf_file = tk.LabelFrame(object, width=256, height=128, text='文件选择')  
        self.lf_file.grid(row=1, column=0, sticky='w', padx=10)

        self.lf_file_label = tk.Label(self.lf_file, 
            width=34, 
            text='已选择:(空)',
            anchor='w', 
            justify='left',
            wraplength=240
            )
        

        def lf_file_bt_click():
            global bookname, fullpath
            SupportedFiletypes = [('所有文件','*.*'), ('mobi文件','*.mobi'), ('文本文件','*.txt'), ('pdf文件','*.pdf')] 
            filename = tk.filedialog.askopenfilename(filetypes=SupportedFiletypes)

            if filename != '':
                filesize = os.path.getsize(filename)/float(1024*1024)  # MB
                if float(filesize) > 50.00:
                    tk.messagebox.showinfo(title='HI', message='文件大小不得超过50MB。')
                    self.lf_file_label.config(text = '已选择:(空)')
                    return
                self.lf_file_label.config(text = '已选择: '+ filename)
                fullpath = filename
                bookname = pinyin.get(os.path.basename(fullpath), format="numerical")

        self.lf_file_bt = tk.Button(self.lf_file,
            text = '选择文件',
            command=lf_file_bt_click
            )
        self.lf_file_bt.place(x=2, y=2)
        self.lf_file_label.place(x=2, y=42)

        # 描述信息
        self.lf_desc = tk.LabelFrame(object, width=256, height=96, text='说明')  
        self.lf_desc.grid(row=2, column=0, sticky='w', padx=10)

        def callback(event):
            webbrowser.open_new(r"https://journal.ethanshub.com/post/category/gong-cheng-shi/-python-kindledian-zi-shu-tui-song#toc_4")

        self.tmp = "目前一些使用的约束"
        self.lf_desc_label = tk.Label(self.lf_desc, 
            fg='blue',
            cursor='hand2',
            width=34, 
            text=self.tmp, 
            anchor='w', 
            justify='left',
            wraplength=250
            )
        self.lf_desc_label.place(x=2, y=2)
        self.lf_desc_label.bind("<Button-1>", callback)
        
        # 按钮
        self.lf_button = tk.Frame(object, width=256, height=96)  
        self.lf_button.grid(row=3, column=0, sticky='w', padx=10)

        def lf_button_bt1_click():
            global mail_host, mail_user, mail_pass, receiver, bookname
            
            smtp.SendToKindle(mail_host, mail_user, mail_pass, receiver, fullpath, bookname)

        self.lf_button_bt1 = tk.Button(self.lf_button,
            text='发送',
            width=12,
            height=2,
            command=lf_button_bt1_click
            )
        self.lf_button_bt1.place(x=20,y=5)

        self.lf_button_bt2 = tk.Button(self.lf_button,
            text='取消',
            width=12,
            height=2,
            command=self.lf_sendinfo.quit
            )
        self.lf_button_bt2.place(x=123,y=5)

# 初始化窗口
root = tk.Tk()
root.title('Sent to Kindle')

width = 276
height = 432
screenwidth = root.winfo_screenwidth()  
screenheight = root.winfo_screenheight()  
size = '%dx%d+%d+%d' % (width, height, (screenwidth - width)/2, (screenheight - height)/2)
root.geometry(size)

SentToKindleUI(root)
root.mainloop()

main.py

import ui as myUI

# 初始化窗口
root = myUI.tk.Tk()
root.title('Sent to Kindle')

width = 276
height = 432
screenwidth = root.winfo_screenwidth()  
screenheight = root.winfo_screenheight()  
size = '%dx%d+%d+%d' % (width, height, (screenwidth - width)/2, (screenheight - height)/2)
root.geometry(size)

myUI.SentToKindleUI(root)
root.mainloop()
展开阅读全文

没有更多推荐了,返回首页