YOLO813

邮件自动化发送程序

    一般来说,发送邮件有几种方法,第一种就是人工来一封封发送,第二种就是借助邮箱的IMAP/SMTP服务,第三种就是通过类似selenium、playwright这种自动化软件来控制浏览器模拟人工发送邮件,今天来说说第三种方法。

    最近有个需求,需要写一个自动化发送邮件的程序,原本以为是很简单的事情,毕竟之前写过类似的gmail邮件发送程序,但是碰到某个公司的企业邮箱确实是费了点时间,特此记录如下:

    先来说说,在一台电脑上批量安装一些库的快捷函数吧,借助国内源安装的速度会快很多

import subprocess
libraries_to_install = ['selenium','pyperclip','pyautogui','pywin32','Pillow']
for library in libraries_to_install:
  try:
    subprocess.check_call(['pip', 'install', '-i', 'https://pypi.tuna.tsinghua.edu.cn/simple', library])
    print(f"Success: {library}")
  except subprocess.CalledProcessError:
    print(f"Failed {library}")

另外,如果你发送的文本中带有emoji等特殊符号,selenium自带的send_keys函数是无法处理的,例如输入一个smiley符号

Message: unknown error: ChromeDriver only supports characters in the BMP

即使你使用Unicode编码,例如

element.send_keys("\U0001F604") # 这会发送一个笑脸Emoji

也是一样的结果,这时候可以使用JS输入的方式:

JS_ADD_TEXT_TO_INPUT = """
  var elm = arguments[0], txt = arguments[1];
  elm.value += txt;
  elm.dispatchEvent(new Event('change'));
  """
browser.execute_script(JS_ADD_TEXT_TO_INPUT, element, "❤")

这样就没问题了,这种方法不仅可以用于解决符号的输入问题,还对繁体中文的输入特别友好,如果你在尝试输入部分繁体中文字符时遇到了问题,也可以考虑使用这种JS输入的方式。

 

如果想要在邮件中插入准备好的图片,也很简单:

import win32clipboard
from PIL import Image
from io import BytesIO

def copy_image_to_clipboard(img_path: str):
    '''输入文件名,执行后,将图片复制到剪切板'''
    image = Image.open(img_path)
    output = BytesIO()
    image.save(output, 'BMP')
    data = output.getvalue()[14:]
    output.close()
    win32clipboard.OpenClipboard()
    win32clipboard.EmptyClipboard()
    win32clipboard.SetClipboardData(win32clipboard.CF_DIB, data)
    win32clipboard.CloseClipboard()

通过将准备好的图片路径通过copy_image_to_clipboard函数复制到剪贴板,再在你想要输入图片的元素下面调用Control+V的快捷键输入图片就可以了

copy_image_to_clipboard(image_path)
content_element.send_keys(Keys.CONTROL, 'v')

最后再简单说说这个邮箱自动化的事情,由于涉及到隐私,就不截图了,该网站的页面大概分为两部分,一部分是主体html页面,使用常规的xpath方法正常取值就可以了,但是右边写信的地方是一个嵌入的iframe,这一点我们通过控制台的元素路径可以看到确实如此。

    因此,我们需要调用selenium的driver.switch_to.frame(iframe_element)函数来跳转到对应的iframe内容中才可以正常取值,iframe_element可以是id,也可以是选中的element元素,注意,此时web界面中的xpath等调试工具是无法使用的,或者说是你即使选中了正确的元素,xpath工具也不会返回结果。

    最简单的做法就是使用浏览器自带的copy xpath元素直接获取对应节点。这里面有一个坑就是之前写惯了id获取元素,这次栽了跟头,例如,//input[@id='subject']这个元素获取输入框理论上是没有问题的对吧,毕竟ID唯一,但就是获取元素失败,而他上一级的元素//div[@class='div_txt'] 又可以获取到内容,我是如何知道上一级元素获取到了呢?通过一级级打印元素值,例如

element = WebDriverWait(driver, timeout=10).until(
    EC.presence_of_element_located((By.XPATH, 'XPATH'))
)
element.get_attribute("style")

最后把二者拼接起来才成功获取到对应的元素,虽然没搞清楚具体原因,但也算是涨个姿势了。

//div[@class='div_txt']//input[@id='subject']

另外,由于该页面还存在iframe元素嵌入iframe,所以如果想要进入更深层的iframe还必须再跳转一次driver.switch_to.frame(iframe_content),如果里面的元素处理完了,返回上一级iframe元素使用driver.switch_to.parent_frame()即可,或者跳转到html主界面driver.switch_to.default_content()。