Tag: JavaScript

  • SSPAI Morning Brief: Cloudflare Acquires Vite Creator Evan You’s VoidZero to Accelerate AI-Powered Web Development

    SSPAI Morning Brief: Cloudflare Acquires Vite Creator Evan You’s VoidZero to Accelerate AI-Powered Web Development

    Morning Brief

    1. Passenger Cars Keep Getting Heavier, Gaining Nearly 400 kg Over 12 Years
    2. iFixit Tears Down Counterfeit Apple Products from Huaqiangbei
    3. Cloudflare Acquires the Company Behind Vite
    4. rsync Sparks Controversy After an AI-Assisted Maintenance Bug
    5. OpenAI Discusses Equity Donation With the U.S. Government
    6. Average Vehicle Age on U.S. Roads Reaches 13 Years
    7. News Worth a Quick Look

    Passenger Cars Keep Getting Heavier, Gaining Nearly 400 kg Over 12 Years

    According to a report by CCTV, passenger cars on the market have been collectively “bulking up” in recent years. Data shows that the average curb weight of new passenger vehicles in China reached 1,704 kg in 2024, nearly 400 kg heavier than the 1,312 kg average recorded in 2012. Moreover, the pace of weight gain continues to accelerate. Between 2020 and 2024 alone, the increase in vehicle weight exceeded the total gain accumulated during the eight-year period from 2012 to 2020. New energy vehicles have become the primary contributors to this trend, with many mainstream models weighing more than two metric tons and some reaching as much as 3.8 tons—heavier than a light-duty truck.

    Experts attribute the increase largely to the automotive industry’s transition toward electrification and consumers’ growing demand for more features. As the industry remains in a rapid phase of EV adoption, components such as traction batteries add significant weight. In today’s mainstream household EVs, battery packs typically weigh between 500 and 650 kg, while long-range models equipped with larger batteries can reach 700 to 800 kg. At the same time, electric motors, power control systems, and battery protection structures integrated into the chassis further increase overall vehicle mass.

    The report notes that excessive vehicle weight reduces efficiency, accelerates tire and brake wear, and can lead to handling issues such as understeer. Energy consumption also rises significantly. For example, a three-ton battery-electric SUV typically consumes more than 20 kWh per 100 kilometers. Industry estimates suggest that reducing vehicle weight by 100 kg can lower energy consumption by approximately 7.5% per 100 kilometers. Additionally, every 20% increase in vehicle weight raises road surface damage rates to roughly 2.07 times the original level, increasing urban road maintenance costs.

    Policymakers have already begun addressing the issue. China’s national standard Limits of Energy Consumption for Electric Vehicles, Part 1: Passenger Cars came into effect on January 1 this year. Through stricter and more systematic performance requirements, the regulation pushes automakers toward meaningful technological upgrades and lower energy consumption. New purchase tax incentive standards set to take effect in 2026 stipulate that battery-electric vehicles with a curb weight exceeding 2,710 kg must achieve CLTC energy consumption of no more than 19.1 kWh per 100 kilometers to qualify for the 50% purchase tax reduction. This measure is expected to curb unrestricted vehicle weight growth to some extent.


    iFixit Tears Down Counterfeit Apple Products from Huaqiangbei

    Recently, iFixit released a video showcasing the teardown of counterfeit Apple products purchased from Shenzhen’s Huaqiangbei electronics market, including fake versions of the Apple Watch Ultra 3, AirPods Max 2, and AirPods Pro 3. All three products together cost less than $100.

    The counterfeit Apple Watch Ultra 3 came preloaded with apps such as Alipay, but many built-in icons—including Compass and Weather—had no actual functionality, merely toggling Night Mode when tapped. On the rear casing, a plastic shell decorated with painted fake sensors replaced the genuine ceramic and sapphire sensor assembly found on the authentic product. The display assembly method also differed significantly from Apple’s design. Inside, the fake watch used a 260 mAh lithium-polymer pouch battery, compared with the genuine model’s 542 mAh battery, and it was directly soldered to the motherboard.

    The counterfeit AirPods Max 2 replaced aluminum components with low-quality plastic. The headband exhibited unusual bending, while the ear cushions appeared prone to rapid wear. Although the device could reproduce a convincing Apple-style pairing animation, it lacked active noise cancellation entirely. Internally, the wiring featured crude soldering work, and steel weights fixed with resin had been added inside the ear cups to imitate the heft of the genuine headphones.

    The counterfeit AirPods Pro 3 used silicone ear tips glued directly onto the earbuds. Inside were pouch batteries and inexpensive soldered copper wires instead of proper flex cables. Apple’s force-sensor controls were replaced with physical buttons, and the counterfeit version lacked both heart-rate sensors and noise-canceling microphones.

    According to iFixit, these counterfeit products not only fail to deliver any of the advanced features they attempt to imitate, but also pose potential fire hazards due to their use of unbranded and uncertified batteries. Built with low-quality components and offering virtually no repairability, they are likely to become electronic waste in a very short time.


    Cloudflare Acquires the Company Behind Vite

    On June 4, Cloudflare announced its acquisition of open-source JavaScript tooling company VoidZero. Core products under the VoidZero umbrella—including the Vite build tool, Vitest testing framework, Rolldown, and Oxc—will be integrated into the Cloudflare Workers platform.

    VoidZero was founded in 2024 by Chinese developer Evan You, creator of the Vue.js framework and Vite. Following the acquisition, You and his team will join Cloudflare’s Emerging Technologies and Incubation (ETI) division. Cloudflare CEO Matthew Prince stated that as AI-assisted code generation continues to grow rapidly, development tools must become lighter and more efficient. The integration aims to provide developers and AI agents with the fastest path from local code creation to deployment across Cloudflare’s global network.

    Addressing concerns from developers about the future of the ecosystem, Cloudflare pledged that Vite and its related tooling would remain vendor-neutral, continue to be released under the MIT License, and stay community-driven. The company also announced a $1 million independent Vite Ecosystem Fund dedicated to supporting open-source contributors and maintainers outside both the Cloudflare and VoidZero organizations.

    According to official figures, Vite currently surpasses 130 million weekly downloads worldwide. Cloudflare’s Vite plugin alone accounts for approximately 13.9 million weekly downloads, representing more than 10% of the total. Financial details of the acquisition have not been disclosed.


    rsync Sparks Controversy After an AI-Assisted Maintenance Bug

    According to The Register, a recent backup failure in a new version of the veteran open-source file synchronization tool rsync has triggered a broader debate about the use of AI in maintaining critical open-source software. Originally created in the 1990s, rsync has become a widely used backup and synchronization component throughout the Unix and Linux ecosystem, with countless enterprise IT departments, NAS devices, and backend scripts relying heavily on its stability.

    After the release of rsync 3.4.3, some users reported that incremental backups were no longer functioning correctly. While investigating the bug, users discovered references to Claude in the project’s commit history, prompting widespread criticism. One developer angrily posted on GitHub, telling the maintainers to “do not vibe fuck up this software,” a comment that quickly received thousands of reactions in agreement.

    In response, rsync creator Andrew Tridgell, a veteran software engineer with 40 years of experience, published an article defending his approach. He acknowledged that the new version did contain a bug affecting certain backup workflows and apologized for the issue, but firmly rejected accusations that he had blindly accepted AI-generated code. Tridgell explained that he primarily uses tools such as Claude for what he described as “grunt work,” including rewriting legacy test suites in Python to strengthen security testing, and emphasized that all generated code undergoes rigorous manual review by himself.

    Tridgell also pointed out that the software maintenance landscape has changed dramatically. Open-source maintainers are now facing a flood of security vulnerability reports, many of which are themselves generated by AI. To cope with the resulting increase in workload, he stated that he has no intention of abandoning AI-assisted development and plans to continue using such tools in the upcoming rsync 3.5 release, which will focus heavily on security improvements.


    OpenAI Discusses Equity Donation With the U.S. Government

    According to CNBC, U.S. President Donald Trump and OpenAI CEO Sam Altman are discussing a proposal that would allow the U.S. government to obtain an equity stake in the company. Negotiations have reportedly been ongoing for more than a year, with OpenAI potentially donating a portion of its shares to establish an investment vehicle similar to a “public wealth fund.”

    The idea was first proposed by Altman to the Trump administration in 2025, with the goal of allowing the American public to directly participate in and benefit from the economic gains generated by advances in artificial intelligence. Trump confirmed the concept to reporters aboard Air Force One on June 5, saying that he is considering arrangements under which certain AI companies would contribute equity so that “the American people become, in effect, partners.” He also revealed plans to meet with executives from several AI companies in the near future. Altman reportedly held extensive discussions with lawmakers and government officials in Washington last week.

    The negotiations come amid growing political interest in how AI-generated wealth should be distributed. Senator Bernie Sanders, for example, previously proposed a plan to impose a one-time 50% equity levy on major AI companies to create a sovereign wealth fund, and recently discussed the idea directly with Altman. During Trump’s second term, the administration has pursued a variety of technology and industrial policies and has already acquired equity stakes in companies including Intel, IBM, as well as several quantum computing and critical minerals firms.

    At the same time, the White House is increasing regulatory oversight of the AI industry. Trump recently signed two executive orders: one directing national security agencies to accelerate the adoption of advanced AI models, and another requiring AI companies to provide government access to major models 30 days before their public release.


    Average Vehicle Age on U.S. Roads Reaches 13 Years

    According to The Wall Street Journal, the average age of vehicles currently on U.S. roads has reached a record 13 years, up 10% from a decade ago. Driven by high new-car prices and economic uncertainty, this trend is reshaping the American automotive industry, pushing automakers and dealerships to focus more on repairs and used-car sales rather than new vehicle purchases.

    The average price of a new vehicle in the United States is now approaching $50,000 (approximately RMB 338,000). Combined with elevated auto loan interest rates, the cost of ownership has become increasingly difficult for consumers who depend on cars for daily transportation. Supply-chain disruptions since the pandemic, along with the recent cooling of the U.S. electric vehicle market, have further encouraged consumers to delay purchases. At the same time, advances in vehicle materials and safety technologies have made modern cars capable of remaining on the road much longer than before.

    As weak new-car sales become the norm, automakers and dealers are increasingly turning their attention to the existing vehicle market. Maintenance and repair operations now account for roughly half of the gross profit generated by U.S. dealerships. Traditional manufacturers such as Ford and General Motors are expanding their presence in the after-sales market. Ford, for example, recently introduced mobile repair services that send technicians directly to customers and uses connected-vehicle data to pre-position replacement parts before a breakdown occurs. Major automakers are also steadily expanding their certified pre-owned vehicle programs.

    Meanwhile, competition in the automotive aftermarket is becoming increasingly intense. Long wait times and a lack of pricing transparency at authorized dealerships have driven more American car owners—including those whose vehicles remain under warranty—to independent repair shops and nationwide quick-service chains. In response, large dealership groups such as Penske are reducing showroom space to create additional service bays and introducing customer perks such as complimentary cappuccinos, Wi-Fi-equipped workspaces, and one-hour express maintenance services in an effort to win customers back.


    News Worth a Quick Look

    • On June 7, leaker Sonny Dickson shared new photos of a foldable iPhone dummy unit, claiming it represents the finalized exterior design. He also suggested that the device may be available in only a single color option: white. Based on the model, the outer display adopts an edge-curved full-screen design. The rear microphone has been upgraded to a five-hole configuration, with the flash positioned beneath it. Meanwhile, the front-facing camera on the inner display has been moved to the upper-left corner.
    • Mark Gurman reports that Apple held a highly confidential internal meeting in early 2025 to address the underwhelming reception of Apple Intelligence and delays affecting the next-generation Siri. Senior executives reportedly realized that the company had fallen significantly behind in the AI race, prompting a major strategic shift. Following the reorganization, Mike Rockwell, who previously led the development of Vision Pro, took over responsibility for Siri, while former AI chief John Giannandrea departed the company earlier this year. After assuming the role, Rockwell quickly restructured the team and, together with software engineering chief Craig Federighi and others, helped facilitate deeper cooperation between Apple and Google. The situation also changed Tim Cook’s management style, with the CEO becoming unusually hands-on in shaping Apple’s AI roadmap.
    • Valve has recently rolled out a major update to the Steam Store homepage. The redesigned storefront features larger and more prominent images, along with more detailed descriptions. A new “Quick Browse” feature allows users to preview recommended games without opening individual store pages. Wishlist and DLC sections, which previously appeared only during sales events and promotions, are now permanently available on the homepage. The update also introduces a “Personalized Calendar” that recommends upcoming releases based on each user’s interests.
  • Give Your Mac Mail a “Brain”: Automatic Fetching, Summarizing, and Reporting

    Give Your Mac Mail a “Brain”: Automatic Fetching, Summarizing, and Reporting

    Although many email clients or plugins already offer per-email summarization and translation, they often lack the ability to generate a global summary of all emails received that day. So, by combining the built-in Mail app on macOS with the Kouzi agent, I created the following workflow:

    “Use AppleScript to fetch email content from the Mail app, call the Kouzi agent to summarize it, and send the summarized results back to my own inbox—executed automatically every day.”

    The implementation can be broken down into three steps:

    1. Fetching email content
    2. Calling the Kouzi agent to summarize
    3. Setting up automation for scheduled execution

    The final effect: every day, you receive a neatly timed email containing the summarized content of all your unread messages, as shown below.

    Temporary older emails were used for testing, so the content appears dated.

    Fetching email content

    To avoid disrupting the reading flow, I’ve placed the full AppleScript used to fetch email content at the end of the article. Here, I’ll highlight the key ideas:

    1. I specified the iCloud account and the newsletter folder in the script, because that’s where all my subscription emails are collected.
    2. The script retrieves only unread emails and marks them as read after successfully extracting their content.
    3. The extracted content is saved as .txt files in a designated folder, using the email subject as the filename. This keeps data storage simple and decouples it from the later AI-processing steps, making debugging easier.
    4. For each email, the script obtains its message-ID and converts it into a URI of the format message://%3c'messageID'%3e. Clicking this URI launches Mail and opens the corresponding email in a pop-up window—very handy for jumping to the original message after reviewing the summary.

    At the end of this step, we have successfully fetched the content of all unread emails in the designated folder and saved them as text files.

    Using the Kouzi Agent to Summarize Email Content

    I use a JavaScript script to read the files saved in the previous step and call the Kouzi API to generate summaries. Of course, you can use DeepSeek or any other provider’s API. I chose Kouzi mainly for its asynchronous query feature. The full script is also included at the end of the article.

    Script Logic

    1. Read all files in the target directory: the script scans through every file in the specified folder.
    2. Filter for .txt files created today: only files created on the current day with a .txt extension are processed.
    3. Read file content and call the conversation API: for each valid file, the script reads the content and submits it to the Kouzi chat API.
    4. Poll until AI processing completes: using the chat/retrieve endpoint, the script continuously checks the processing status until it becomes completed.
    5. Fetch and organize AI results: through the chat/message/list endpoint, the script retrieves the AI’s response and formats it as HTML, accumulating everything into an allContents variable.
    6. Send an email once all files are processed: after processing all valid files, the script uses nodemailer to send an email containing all summaries generated that day.
    7. Email includes unread count and AI summaries: the subject is the current date, and the body includes the number of unread emails plus all AI-generated summaries.

    Kouzi Agent Setup and Invocation
    Create an agent inside the Kouzi workspace and configure the agent’s prompt. In the address bar of that setup page, you’ll find the Bot_ID, which is used as a parameter when calling the API.

    Kouzi API calls consist of three steps: initiating a conversation, checking the status, and retrieving the result.

    The first step, initiating a conversation, is essentially submitting a processing request to Kouzi.

    Here you need to provide your API Key, Bot_ID, and a custom User_ID. Kouzi will return a Chat_ID and a Conversation_ID as unique identifiers.

    The second step, checking the status, is like asking Kouzi whether the request has finished processing. Only after receiving a completed status should you fetch the result; otherwise, you may end up retrieving incomplete output.

    At this stage, you must provide the Chat_ID and Conversation_ID returned in the previous step.

    The third step, retrieving the result, is where you obtain the final output from the agent. You again supply the Chat_ID and Conversation_ID from step one to fetch the completed response.

    Configure Automation for Scheduled Execution

    With the AppleScript for fetching email content and the JavaScript script for calling the Kouzi agent ready, the next step is to create an automated workflow that runs them regularly.

    Open the Automator app and create a new Calendar Alarm workflow:

    Add a Run AppleScript action, paste in your AppleScript code, and remember to wrap it with the execution block, like this:

    on run {input, parameters}
        -- AppleScript code
    end run

    Add a Run Shell Script action, navigate to the folder containing your script, and execute the JavaScript file:

    cd /path/to/script
    /opt/homebrew/bin/node scriptName.js

    Add a Show Notification action to confirm the workflow has finished running.

    The Automator configuration looks like this:

    After creating it, open the Calendar app and make a new event. Set it to repeat daily, and under the Alert section choose:
    Custom → Open File → Other… → select your Automator workflow.
    This way, it will run automatically each day.

    With this, the entire setup is complete. However, be mindful of privacy and security when using it—it’s best to apply this workflow only to newsletter or public information emails, and avoid uploading private messages containing sensitive personal data.

    AppleScript for Fetching Email Content

    tell application "Mail"
    	-- Get the "newsletter" mailbox under the iCloud account
    	set theAccount to account "iCloud"
    	set theMailbox to mailbox "newsletter" of theAccount
    
    	-- Get all unread emails in the "newsletter" mailbox
    	set unreadMessages to (every message of theMailbox whose read status is false)
    
    	-- Iterate through all unread emails
    	repeat with eachMessage in unreadMessages
    		-- Use the email subject as part of the filename
    		set theSubject to subject of eachMessage
    		-- Get the email content and convert it to plain text
    		set theContent to content of eachMessage
    		set plainTextContent to do shell script "echo " & quoted form of theContent & " | textutil -convert txt -stdin -stdout"
    
    		-- Get the message-ID and generate a URI
    		set messageID to message id of eachMessage
    		set messageURL to "message://" & "%3c" & messageID & "%3e"
    	
    		-- Create a filename by removing illegal characters
    		set cleanSubject to do shell script "echo " & quoted form of theSubject & " | tr -d '\"/:<>?\\|+[]{};=,'"
    		set fileName to (cleanSubject & ".txt")
    	
    		-- Use the specified POSIX path and convert it to AppleScript path format
    		set savePath to POSIX file "path to save txt files"
    	
    		-- Full file path
    		set filePath to (savePath as string) & fileName
    	
    		-- Write the email content to the txt file at the specified path
    		try
    			set fileReference to open for access file filePath with write permission
    			write plainTextContent to fileReference starting at eof
    	
    			-- Append the email URI at the end of the file
    			write return & "emailURL=" & messageURL to fileReference starting at eof
    			close access fileReference
    		on error errMsg
    			close access file filePath
    			display dialog "Error: " & errMsg
    		end try
    	
    		-- Mark the email as read
    		set read status of eachMessage to true
    	end repeat
    end tell
    
    return input

    Script for Calling the Kouzi Agent

    const fs = require('fs');
    const path = require('path');
    const axios = require('axios');
    const nodemailer = require('nodemailer');
    
    const directoryPath = 'Directory path to store text files'; // The directory path to store text files
    const today = new Date().toLocaleDateString('zh-CN'); // Get system local date
    
    let allContents = ''; // Used to store all returned contents from the query API
    let fileCounter = 0; // Used for file counting
    let validFileCount = 0; // Valid file count
    let emailSent = false; // A flag to track whether the email has been sent
    
    // Read files under the specified directory
    fs.readdir(directoryPath, async (err, files) => {
      if (err) {
        console.error('Failed to read directory:', err);
        return;
      }
    
      const filePromises = files.map(file => processFile(file));
      await Promise.all(filePromises);
      checkAllFilesProcessed();
    });
    
    async function processFile(file) {
      const filePath = path.join(directoryPath, file);
    
      try {
        const stats = await fs.promises.stat(filePath);
        const fileCreationDate = stats.birthtime.toLocaleDateString('zh-CN'); // Get file creation date
        if (fileCreationDate === today && path.extname(file) === '.txt') {
          validFileCount++; // Increase valid file count
          const content = await fs.promises.readFile(filePath, 'utf8');
          await callAIAPI(content, file);
        }
      } catch (err) {
        console.error('Error while processing file:', err);
      }
    }
    
    // Use async/await to optimize async handling
    async function callAIAPI(content, fileName) {
      const apiUrl = 'https://api.coze.cn/v3/chat';
      const headers = getHeaders();
      
      // Choose bot_id depending on content length
      const botId = content.length > 32000 ? 'bot_id' : 'bot_id';
      
      const data = {
        bot_id: botId, // Dynamic bot_id
        user_id: '**',
        stream: false,
        auto_save_history: true,
        additional_messages: [
          {
            role: 'user',
            content: content,
            content_type: 'text'
          }
        ]
      };
    
      let attempts = 0;
      const maxAttempts = 5;
    
      while (attempts < maxAttempts) {
        try {
          const response = await axios.post(apiUrl, data, { headers, timeout: 5000 });
          console.log('API response:', response.data);
          const { id, conversation_id } = response.data.data;
          await new Promise(resolve => setTimeout(resolve, 1000)); // Add wait time: 1 second
          await pollConversationStatus(id, conversation_id, fileName);
          break;
        } catch (error) {
          attempts++;
          console.error(`API call error (attempt ${attempts}/${maxAttempts}):`, error);
          if (attempts >= maxAttempts) {
            console.error('Still failed after multiple attempts');
          }
        }
      }
    }
    
    // Extract headers function
    function getHeaders() {
      return {
        'Authorization': 'Bearer api_key',
        'Content-Type': 'application/json'
      };
    }
    
    // Poll conversation detail interface
    async function pollConversationStatus(chat_id, conversation_id, fileName) {
      const retrieveUrl = `https://api.coze.cn/v3/chat/retrieve?chat_id=${chat_id}&conversation_id=${conversation_id}`;
      const headers = getHeaders();
    
      let pollCount = 0;
      const maxPollCount = 120;
    
      return new Promise((resolve, reject) => {
        const intervalId = setInterval(async () => {
          pollCount++;
          if (pollCount > maxPollCount) {
            clearInterval(intervalId);
            reject(new Error(`Polling timeout, chat_id: ${chat_id}, conversation_id: ${conversation_id}`));
            return;
          }
    
          try {
            const response = await axios.get(retrieveUrl, { headers });
            // Cancel printing poll logs
            // console.log('Dialog detail API response:', response.data);
            if (response.data.data.status === 'completed') {
              clearInterval(intervalId);
              await queryAIAPI(chat_id, conversation_id, fileName);
              resolve();
            }
          } catch (error) {
            console.error('Dialog detail API error:', error);
            clearInterval(intervalId);
            reject(error);
          }
        }, 1000); // Poll every second
      });
    }
    
    // Call query interface
    async function queryAIAPI(chat_id, conversation_id, fileName) {
      const queryUrl = `https://api.coze.cn/v3/chat/message/list?chat_id=${chat_id}&conversation_id=${conversation_id}`;
      const headers = getHeaders();
    
      try {
        const response = await axios.get(queryUrl, { headers });
        console.log('Query API response:', response.data);
        const contents = response.data.data
          .filter(item => item.type === 'answer') // Filter items of type 'answer'
          .map(item => item.content)
          .join('\n');
    
        // Read last line of file to get MessageID
        const filePath = path.join(directoryPath, fileName);
        const fileContent = await fs.promises.readFile(filePath, 'utf8');
        const lastLine = fileContent.trim().split('\n').pop();
        const messageId = lastLine.split('=')[1]; // Extract MessageID
    
        fileCounter++; // Increase file count
        console.log(`File count: ${fileCounter}`);
        const fileNameWithoutExt = fileName.replace(/\.[^/.]+$/, ""); // Remove extension
        allContents += `<h3><a href="${messageId}">${fileCounter}. ${fileNameWithoutExt}</a></h3><br>${contents.replace(/\n/g, '<br>')}<br><br>`;
      } catch (error) {
        console.error('Query API error:', error);
      }
    }
    
    // Check if all files are processed
    function checkAllFilesProcessed() {
      if (fileCounter === validFileCount && !emailSent) {
        emailSent = true; // Mark email as sent
        sendEmail(allContents); // Call send email function
      }
    }
    
    // Send email
    function sendEmail(contents) {
      const transporter = nodemailer.createTransport({
        host: 'smtp.qq.com',
        port: 465,
        secure: true, // Use SSL
        auth: {
          user: 'Email address', // Your email address
          pass: 'Email authorization code' // Your email authorization code
        }
      });
    
      const mailOptions = {
        from: 'Email address', // Sender email address
        to: 'Email address',    // Recipient email address
        subject: `${today} Daily Email Summary`, // Email subject
        html: `Unread email count: ${validFileCount}<br><br>${contents}` // HTML content
      };
    
      transporter.sendMail(mailOptions, (error, info) => {
        if (error) {
          return console.error('Failed to send email:', error);
        }
        console.log('Email sent:', info.response);
      });
    }